/**
* Copyright (c) 2014, Oracle and/or its affiliates.
* All rights reserved.
*/
/*jslint browser: true*/
/**
* @export
* @class oj.Collection
* @classdesc Collection of Model objects
*
* @param {Array=} models Set of model objects to put into collection at construction time
* @param {Object=} options Passed through to the user's initialize routine, if any, upon construction
* @constructor
* @mixes oj.Events
*/
oj.Collection = function(models, options) {
if (oj.Collection._justExtending) {
return;
}
// Initialize
oj.Collection._init(this, models, options, null);
};
// Subclass from oj.Object
oj.Object.createSubclass(oj.Collection, oj.Object, "oj.Collection");
/**
* @desc Property specifying the [model]{@link oj.Model} class object used by the collection
*
* @type Object
* @export
*/
oj.Collection.prototype.model = null;
/**
* @desc Function specifying how to construct the id for models in this collection. Override to change the construction of the id.
* @type {(function(Object):(String)|null)}
* @export
*/
oj.Collection.prototype.modelId = function(attrs) {
var model = this['model'];
if (model && attrs) {
return attrs[model['idAttribute'] || model.prototype['idAttribute'] || 'id'];
}
return null;
};
/**
* @export
* @desc Total number of models in the collection. When the collection is virtual, not all of the models may be locally available.
*
* @type number
*/
oj.Collection.prototype.length = undefined;
/**
* @export
* @desc Direct access to the collection's list of models objects<br/>
* Note that this property should not be used directly when a collection is virtual, as
* automatic fetches will not be triggered for undefined elements in the model. Use at() instead.
*
* @type Array
*/
oj.Collection.prototype.models = undefined;
// Tracking indices used
oj.Collection.prototype._modelIndices = [];
/**
* @export
* @desc The data service's server URL.
*
* @type String
*/
oj.Collection.prototype.url = null;
/**
* @export
* @desc Changes that have occured due to adds/removes since the last fetch. This is a list of indicies that have changed (location at which a model was added, deleted, or set). They do not shift with subsequent operations
* @type Array
*/
oj.Collection.prototype.changes = [];
/**
* A callback to allow users to customize the data service URLs. The callback should accept these parameters:<p>
* <b>operation</b>: one of "create", "read", "update", "patch", or "delete", indicating the type of operation for which to return the URL<p>
* <b>collection</b>: the oj.Collection object requesting the URL<p>
* <b>options</b>: any of the following properties:<br>
* <ul>
* <b>recordID</b>: id of the record involved, if relevant<br>
* <b>fetchSize</b>: how many records to return. If not set, return all.<br>
* <b>startIndex</b>: Starting record number of the set to return.<br>
* <b>startID</b>: Retrieve records starting with the record with the given unique ID.<br>
* <b>since</b>: Retrieve records with timestamps after the given timestamp.<br>
* <b>until</b>: Retrieve records with timestamps up to the given timestamp. Default is "until"<br>
* <b>sort</b>: field(s) by which to sort, if set<br>
* <b>sortDir</b>: sort ascending or descending (asc/dsc)<br>
* <b>query</b>: a set of attributes indicating filtering that should be done on the server. See [where]{@link oj.Collection#where} for complete documentation of query values<br>
* <b>all</b>: true (along with 'query', above) indicates that this is a findWhere or where type call that is expecting all models meeting the query condition to be returned<br>
* </ul>
* <p>
* customURL callbacks should return either: null, in which case the default will be used; a url string, which will be used with the standard
* HTTP method for the type of operation, or an Object with any other attributes that should be passed to the ajax call.<br>
* This object must at minimum include the URL, and other attributes as follows:<br>
* <ul>
* <b>url</b>: the custom URL string<br>
* <b>type</b>: (optional) a string indicating the type of HTTP method to use (GET, POST, DELETE, etc.)<br>
* <b>(other)</b>: (optional) any other attributes to pass in the ajax call<br>
* </ul>
* <p>
* @type {(function(string,Object,Object):(string|Object|null))|null}
* @export
*/
oj.Collection.prototype.customURL = null;
/**
* A callback allowing users to extract their own paging/virtualization return values from the server's response.<p>
* It should accept these parameters:<p>
* <b>response</b>: the response coming back from the fetch call<p>
* <p>
* The callback should return either null, in which case the collection will look for the default properties, or
* an object containing one or more of the following attribute/value pairs (note that the Collection will look back to the response for default paging return properties if
* not found in the returned object):<br>
* <ul>
* <b>totalResults</b>: the total number of records available on the server side, not just in the current result. By default the collection looks in the response for "totalResults"<br>
* <b>limit</b>: the actual fetchSize from the server. This may not be the client's fetchSize or the number of records in the current result. By default the collection looks in the response for "limit". This becomes the collection's "lastFetchSize" property<br>
* <b>count</b>: the actual number of records returned by the server in the last result. This becomes the collection's "lastFetchCount". By default the collection looks in the response for "count".<br>
* <b>offset</b>: the actual starting record number of the current result. By default the collection looks in the response for "offset"<br>
* <b>hasMore</b>: boolean indicating whether or not there are more records available beyond the current result. By default the collection looks in the response for "hasMore"<br>
* </ul>
* <p>
*
* @type {(function(Object):(Object|null)|null)}
* @export
*/
oj.Collection.prototype.customPagingOptions = null;
/**
* @export
* @desc The server's fetch size. This may not match [fetchSize]{@link oj.Collection#fetchSize}.
*
* @type number
*/
oj.Collection.prototype.lastFetchSize = undefined;
/**
* @export
* @desc Indicates whether or not there are more records available on the server, at indices beyond the last fetch.
*
* @type boolean
*/
oj.Collection.prototype.hasMore = false;
/**
* @export
* @desc The total number of records available for this collection regardless of whether they have been fetched or not. For non-virtual collections this will equal the length.
*
* @type number
*/
oj.Collection.prototype.totalResults = undefined;
/**
*
* @export
* @desc The number of records actually fetched the last time the collection fetched from the server. This may or may not match [fetchSize]{@link oj.Collection#fetchSize} or [lastFetchSize]{@link oj.Collection#lastFetchSize}
*
* @type number
*/
oj.Collection.prototype.lastFetchCount = undefined;
/**
* @export
* @desc For virtual collections, the number of records to be kept in memory at any one time. The default of -1 indicates that no records are thrown out
*
* @type number
*/
oj.Collection.prototype.modelLimit = -1;
/**
* @export
* @desc The actual starting index number at which models from the last server fetch were inserted into the collection.
*
* @type number
*/
oj.Collection.prototype.offset = undefined;
/**
* @export
* @desc The number of records to be fetched from the server in any one round trip. The server's fetch size comes back as the "limit" property. The default value of -1 indicates that virtualization is not being used or is not available, and all records will be fetched.<br>
* The number of records actually fetched comes back as [count]{@link oj.Collection#count}<br>
*
* @type number
*/
oj.Collection.prototype.fetchSize = -1;
/**
* @export
* @desc Sort direction for string-based sort comparators (model attribute names). A value of 1 indicates ascending sorts, -1 indicates descending. The default is 1 (ascending).<br>
* Users should call sort() after changing sort direction to ensure that models in the collection are sorted correctly, or, for virtual collections, that there are no left over models in an incorrect order.
*
* @type number
*/
oj.Collection.prototype.sortDirection = 1;
/**
* @export
* @desc If set to a string, sort the collection using the given attribute of a model.<p>
* If set to a function(Model), the function should return a string giving the model attribute by which the sort should take place.<p>
* If set to a function(Model1, Model2), then this function is called comparing Model1 and Model2 (see the JavaScript array.sort() for details)<p>
* In the virtual case, comparator must be a string-based field comparator, which will be passed to the server.<p>
* Users should call sort() after making any changes to the comparator to ensure that the models are correctly sorted, or that there are no leftover models sorted incorrectly in the virtual case.
*
* @type {String|function(Object)|function(Object,Object)}
*/
oj.Collection.prototype.comparator = null;
/**
* @export
* @desc Set to true if sort is supported. This will only be false if the collection is connected to a paging REST service that does
* not support sorting
*
* @type boolean
*/
oj.Collection.prototype.sortSupported = true;
oj.Collection.prototype.Init = function()
{
oj.Collection.superclass.Init.call(this);
};
/**
* Create a new, specific type of Collection object to represent a collection of records from a JSON data set.
* @param {Object=} properties Properties for the new Collection class.<p>
* <b>parse</b>: a user callback function to allow parsing of the JSON collection after it's returned from the data service<br>
* <b>model</b>: the specific type of [model]{@link oj.Model} object to use for each member of the collection<br>
* <b>url</b>: the URL string to use to get records from the data service<br>
* <b>initialize</b>: a user callback function to be called when this collection is created. Called with collection, models, options.<br>
* <b>comparator</b>: a user callback used on sort calls. May also be set to false to prevent sorting. See [comparator]{@link oj.Collection#comparator}<br>
* <b>fetchSize</b>: the number of records to be fetched on each round trip to the server. Overrides [fetchSize]{oj.Collection#fetchSize}. If not set, the collection will not consider itself virtual<br>
* <b>modelLimit</b>: the number of records to be held in memory at any one time, if virtualization is in force. The default is all records. This uses an LRU algorithm to determine which to roll off as more records are added.<br>
* @param {Object=} classProperties optional properties that get attached to the constructor of the extended Collection
* @return {function(new:Object, ...)} new Collection object
* @this {oj.Collection}
* @export
*/
oj.Collection.extend = function(properties, classProperties)
{
oj.Collection._justExtending = true;
var obj = new oj.Collection();
oj.Collection._justExtending = false;
// Add regular properties from this "parent"
//oj.Events.Mixin(obj, this.prototype);
$.extend(obj, this.prototype);
var Collection;
if (properties && properties['constructor'] && properties.hasOwnProperty('constructor')) {
Collection = properties['constructor'];
}
else {
Collection = function(models, options) {
oj.Collection._init(this, models, options, properties);
}
}
if (classProperties) {
var prop;
for (prop in classProperties) {
if (classProperties.hasOwnProperty(prop)) {
Collection[prop] = classProperties[prop];
}
}
}
if (properties) {
var prop;
for (prop in properties) {
if (properties.hasOwnProperty(prop)) {
obj[prop] = properties[prop];
}
}
}
$.extend(Collection, this);
Collection.prototype = obj;
// Allow extending resulting obj
Collection.extend = oj.Collection.extend;
Collection.prototype.constructor = Collection;
return Collection;
};
oj.Collection._init = function(collection, models, options, properties) {
var prop, i, optionlist, modelList;
collection.Init();
// Augment with Event
oj.Events.Mixin(collection);
// First, copy all properties passed in
if (properties) {
for (prop in properties) {
if (properties.hasOwnProperty(prop)) {
collection[prop] = properties[prop];
}
}
}
// Check options
options = options || {};
optionlist = ["url", "comparator", "model", oj.Collection._FETCH_SIZE_PROP, "modelLimit", "customURL"];
for (i = 0; i < optionlist.length; i++) {
if (options.hasOwnProperty(optionlist[i]) && options[optionlist[i]] !== undefined) {
collection[optionlist[i]] = options[optionlist[i]];
}
}
if (collection._getFetchSize(null) === undefined) {
collection.setFetchSize(-1);
}
if (collection['modelLimit'] === undefined) {
collection.setModelLimit(-1);
}
collection['hasMore'] = false;
collection.lruCount = 0;
collection._setModels([], true);
if (options['parse']) {
models = collection['parse'](models);
}
if (models != null) {
options.noparse = true;
modelList = (models instanceof Array) ? models : [models];
for (i = 0; i < modelList.length; i=i+1) {
collection.add(modelList[i], options);
}
}
collection._setLength();
if (properties && properties['initialize']) {
properties['initialize'].call(collection, models, options);
}
};
// Placeholder for event mixins
oj.Collection.prototype.on = function (event, callback) {};
oj.Collection.prototype.OnInternal = function(event, callback, context, listenTo, ignoreSilent) {};
oj.Collection.prototype.TriggerInternal = function (silent, event, arg1, arg2, options) {};
// Fire request event
oj.Collection.prototype._fireRequest = function(collection, xhr, options, silent) {
this.TriggerInternal(silent, oj.Events.EventType['REQUEST'], collection, xhr, options);
};
oj.Collection.prototype._resetChanges = function() {
this['changes'] = [];
};
oj.Collection.prototype._setChangeAt = function(start, count) {
for (var at = start; at < start+count; at++) {
if (this['changes'].indexOf(at) === -1) {
this['changes'].push(at);
this['changes'].sort(function(a,b) { return a-b;});
}
}
};
oj.Collection.prototype._setModels = function(models, clearing) {
this['models'] = models;
if (clearing) {
this._modelIndices = [];
this._resetChanges();
}
else {
for (var i = 0; i < models.length; i++) {
if (models[i]) {
this._modelIndices.push(i);
}
}
}
};
/*oj.Collection.prototype._setModel = function(index, model) {
this._getModels()[index] = model;
};*/
oj.Collection.prototype._getModels = function() {
return this['models'];
};
oj.Collection.prototype._getModelsLength = function() {
return this._getModels().length;
};
// Designed to check if index exceeds the length of the models. If we're in a virtual and "no totalResults" case, we're never over the upper limit
oj.Collection.prototype._overUpperLimit = function(index) {
if (index < this._getModelsLength()) {
return false;
}
if (this.IsVirtual()) {
if (!this._hasTotalResults() || this._getModelsLength() === 0) {
return false;
}
}
return true;
};
oj.Collection.prototype._hasTotalResults = function() {
return oj.Collection._defined(this['totalResults']);
}
oj.Collection._defined = function(value) {
return value != null;
}
oj.Collection.prototype._pushModel = function(model) {
this._getModels().push(model);
this._modelIndices.push(this._getModelsLength()-1);
this._setChangeAt(this._getModelsLength()-1, 1);
};
oj.Collection.prototype._pushModels = function(model) {
// Model is being added to the end, it should be made the head
this._makeModelHead(model);
this._pushModel(model);
this.lruCount++;
model.SetIndex(this._getModelsLength()-1);
};
oj.Collection.prototype._reduceLRU = function(removed) {
if (removed) {
for (var i = 0; i < removed.length; i++) {
if (removed[i]) {
this.lruCount--;
}
}
}
};
/**
* @param {number} start
* @param {number} count
* @param {Object=} model
* @private
*/
oj.Collection.prototype._spliceModels = function(start, count, model) {
// Clean up prev/next links for models being removed
for (var i = start; i < start + count; i++) {
this._removePrevNext(this._getModel(i));
}
if (model === undefined) {
this._reduceLRU(this._getModels().splice(start, count));
this._spliceModelIndices(start, start+count-1);
}
else {
this._reduceLRU(this._getModels().splice(start, count, model));
this._spliceModelIndices(start, start+count-1);
this._insertModelIndex(start);
this._makeModelHead(model);
}
this._setChangeAt(start, count);
if (this.lruCount < 0) {
this.lruCount = 0;
}
this._realignModelIndices(start);
};
oj.Collection.prototype._getModel = function(index) {
return this._getModels()[index];
};
// Realign all the indices of the models (after sort for example)
oj.Collection.prototype._realignModelIndices = function(start) {
var index;
for (var i = 0; i < this._modelIndices.length; i++) {
index = this._modelIndices[i];
if (index >= start && this._getModel(index)) {
this._getModel(index).SetIndex(index);
}
}
};
// Update next/prev pointers as though the given model were being removed
oj.Collection.prototype._removePrevNext = function(model) {
if (!model) {
return;
}
var oldPrev = model.GetPrevious();
var oldNext = model.GetNext();
// Link the two surrounding previous/next elements to each other, because this one is being replaced and moved
// to the head
if (oldPrev) {
oldPrev.SetNext(oldNext);
}
else {
// This element used to be the head
this.head = oldNext;
}
if (oldNext) {
oldNext.SetPrevious(oldPrev);
}
else {
// This element used to be the tail
this.tail = oldPrev;
}
};
oj.Collection.prototype._makeModelHead = function(model) {
// Make this new model the most recently used: the head
model.SetNext(this.head);
if (this.head) {
this.head.SetPrevious(model);
}
else {
// No head: list is empty-->make tail the same element
this.tail = model;
}
model.SetPrevious(null);
this.head = model;
};
// Mark the model index tracker as having a used slot
oj.Collection.prototype._setModelIndex = function(index) {
if (this._modelIndices.indexOf(index) === -1) {
this._modelIndices.push(index);
}
}
oj.Collection.prototype._verifyIndices = function() {
var models = this._getModels();
var count = 0;
for (var i = 0; i < models.length; i++) {
if (models[i]) {
count++;
var found = this._modelIndices.indexOf(i);
if (found === -1) {
console.log("ERROR: Could not find:" + i);
}
}
}
if (count !== this._modelIndices.length) {
console.log("ERROR: Model index count is wrong");
}
console.log("all ok");
}
// Insert the given model at the given index
oj.Collection.prototype._insertModelIndex = function(start) {
// Up all the indices of models with index greater than start
for (var i = 0; i < this._modelIndices.length; i++) {
if (this._modelIndices[i] >= start) {
this._modelIndices[i]++;
}
}
// Now add the new one
this._modelIndices.push(start);
}
// Splice out the given model index
oj.Collection.prototype._spliceModelIndices = function(start, end) {
if (end === undefined) {
end = start;
}
this._clearModelIndices(start, end);
var count = end-start+1;
// Reduce the indexes of any models above the endpoint by the number of models removed
for (var i = 0; i < this._modelIndices.length; i++) {
if (this._modelIndices[i] > end) {
this._modelIndices[i] -= count;
}
}
}
// Clear the given model index
oj.Collection.prototype._clearModelIndices = function(start, end) {
if (end === undefined) {
end = start;
}
// Knock out any of the deleted model's indexes from the list
for (var i = start; i <= end; i++) {
var idx = this._modelIndices.indexOf(start);
if (idx > -1) {
this._modelIndices.splice(idx, 1);
}
}
}
oj.Collection.prototype._setModel = function(index, model) {
var oldModel = this._getModel(index);
this._removePrevNext(oldModel);
if (!oldModel) {
// Newly "inserted" model
this.lruCount++;
}
this._setChangeAt(index, 1);
this._getModels()[index] = model;
if (model) {
this._setModelIndex(index);
model.SetIndex(index);
this._makeModelHead(model);
}
};
// Clean off n models from tail (oldest) of prev/next list
oj.Collection.prototype._clearOutModels = function(n) {
var current = this.tail, index, model;
var i = 0;
this.tail = null;
while (current && i < n) {
// Erase this model from collection, iff it hasn't changed
index = current.GetIndex();
model = this._getModel(index);
if (!(model && model.hasChanged())) {
this.lruCount--;
if (index > -1) {
//this._getModels()[index] = undefined;
this._setModel(index, undefined);
this._clearModelIndices(index, index);
}
// Clear its pointers
current.SetNext(null);
current = current.SetPrevious(null);
i++;
}
else {
// Lock down the tail to this one we're not deleting
if (!this.tail) {
this.tail = current;
}
current = current.GetPrevious();
}
}
// Make sure we set tail if not already set
if (!this.tail) {
this.tail = current;
}
if (this.lruCount < 0) {
this.lruCount = 0;
}
if (this.lruCount === 0) {
this.head = null;
this.tail = null;
}
};
// Reset the LRU list
oj.Collection.prototype._resetLRU = function() {
this.lruCount = 0;
this.head = null;
this.tail = null;
};
// Make sure we have room in the LRU
oj.Collection.prototype._manageLRU = function(incoming) {
if (this.IsVirtual()) {
var limit = this._getModelLimit();
if (limit > -1) {
if (this.lruCount + incoming > limit) {
// Must flush the amount over the limit
this._clearOutModels(this.lruCount + incoming - limit);
}
}
}
};
/**
* @export
* @desc Return a copy of the Collection
*
* @return {Object} copy of the Collection
*/
oj.Collection.prototype.clone = function() {
return this._cloneInternal(true);
};
oj.Collection.prototype._cloneInternal = function(withProperties) {
var c = new this.constructor(), i;
// Only copy locally if virtual
var model;
if (this.IsVirtual()) {
c = this._copyFetchProperties(c);
c._resetModelsToFullLength();
}
c = this._copyProperties(c);
if (withProperties) {
// Try to copy models only if told to--we may only need the shell of the collection with properties
// Make a copy of the model indices (it will be modified by this process)
var indices = [];
for (i = 0; i < this._modelIndices.length; i++) {
indices.push(this._modelIndices[i]);
}
// Sort them in reverse order so we eliminate the higher indexes first so as not to disrupt the position of the earlier ones
indices.sort(function(a,b) {
return a-b;
});
var index;
for (i = 0; i < indices.length; i++) {
index = indices[i];
model = this._atInternal(index, null, true, false);
if (model) {
c._addInternal(model.clone(), {'at':index}, true, false);
}
}
}
return c;
};
// Copy critical properties in clone
oj.Collection.prototype._copyProperties = function(collection) {
var props = ['comparator', 'model', 'modelId'], prop, i;
for (i = 0; i < props.length; i++) {
prop = props[i];
collection[prop] = this[prop];
}
return collection;
};
// Copy critical fetch properties in clone
oj.Collection.prototype._copyFetchProperties = function(collection) {
var props = ['totalResults', 'hasMore', oj.Collection._FETCH_SIZE_PROP], prop, i;
for (i = 0; i < props.length; i++) {
prop = props[i];
collection[prop] = this[prop];
}
return collection;
};
oj.Collection.prototype._getLength = function() {
return this['length'];
};
oj.Collection.prototype._setLength = function()
{
var modelsLen = this._getModelsLength();
this['length'] = modelsLen;
if (!this.IsVirtual()) {
this['totalResults'] = modelsLen;
}
};
/**
* Create a model instance using the model property if set
* @param {Object} collection
* @param {Object=} attrs
* @param {Object=} options
* @returns {null||Object}
* @private
*/
oj.Collection._createModel = function(collection, attrs, options) {
if (collection['model']) {
return $.isFunction(collection['model']) ? new collection['model'](attrs, options) : new collection['model'].constructor(attrs, options);
}
return null;
};
oj.Collection.prototype._newModel = function(m, parse, options, ignoreDefaults) {
var newModel = null, validationValue;
var opt = options || {};
// opt.noparse = !parse;
opt.ignoreDefaults = ignoreDefaults;
if (m instanceof oj.Model) {
newModel = m;
}
else {
if (this['model']) {
// model is defined
newModel = oj.Collection._createModel(this, m, opt);
}
else {
// Set this collection on the model
opt['collection'] = this;
newModel = new oj.Model(m, opt);
}
}
// Validate
if (opt['validate'] && newModel['validate']) {
validationValue = newModel['validate'](newModel.attributes);
if (validationValue) {
opt['validationError'] = validationValue;
this.TriggerInternal(false, oj.Events.EventType['INVALID'], newModel, validationValue, opt);
return null;
}
}
return newModel;
};
/**
* Add a model or models to the end of the collection.<br>
* Events:<p>
* <ul>
* <b>add</b>: fired for each model added, passing the collection, model added, and options<br>
* <b>alladded</b>: fired after all models have been added, passing the collection, array of models added, and options<br>
* </ul>
* <p>
* Note that for virtual collections, if a new model is added after being saved up to the server, no add event will be fired as the
* collection will already "see" the model as existing. Note that a warning will be logged if this add is not a force, not merging, and duplicate IDs are found.
* @param {Object|Array} m Model object (or array of models) to add. These can be already-created instance of the oj.Model object, or sets of attribute/values, which will be wrapped by add() using the collection's model.
* @param {Object=} options <b>silent</b>: if set, do not fire events<br>
* <b>at</b>: splice the new model into the collection at the value given (at:index)<br>
* <b>merge</b>: if set, and if the given model already exists in the collection (matched by [id]{@link oj.Model#id}), then merge the attribute/value sets, firing change events<br>
* <b>sort</b>: if set, do not re-sort the collection even if the comparator is set.<br>
* <b>force</b>: if set to true, do an add to the collection no matter whether the item is found or not<br>
* <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not<br>
*
* @returns {Promise|Array} The model or models added to the collection (or found/merged if appropriate). If deferred or virtual, return the model or models added in a promise when the set has completed
* @export
*/
oj.Collection.prototype.add = function(m, options) {
this._manageLRU(1);
var opt = options || {};
return this._handlePromise(this._addInternal(m, options, false, opt['deferred']));
};
// fillIn: true indicates that we're just trying to use add() after a fetch to
// insert into a preallocated list of models, not truly do an add/merge from the API
oj.Collection.prototype._addInternal = function(m, options, fillIn, deferred) {
// Get options
options = options || {};
var modelArray = [],
at = options['at'],
silent = options['silent'],
force = options['force'],
i, index, cid,
merge = options['merge'] || false,
sort = options['sort'], needSort = true, added = false, addedModels = [],
modelReturnList = [];
if (at !== undefined && at < 0) {
// Normalize it using the length-- another BackboneJS test case
at += this._getLength() + 1;
}
if (m instanceof Array) {
modelArray = m;
}
else {
modelArray.push(m);
}
function addToCollection(collection, newModel) {
if (at === undefined) {
collection._pushModels(newModel);
index = collection._getModelsLength()-1;
collection._getModel(index).SetCid();
}
else {
index = at;
if (collection.IsVirtual() && fillIn) {
// Array has been preallocated in this case
collection._setModel(index, newModel);
}
else {
collection._spliceModels(index, 0, newModel);
}
collection._getModel(index).SetCid();
// Increment at so that later models will be added right after their predecessors, if an array is passed in
at = at + 1;
}
if (newModel.GetCollection() === undefined) {
newModel.SetCollection(collection);
}
collection._setLength();
collection._listenToModel(newModel);
added = true;
}
function resortAndFireEvents(collection, existingModel, modelFoundInCollection, newModel, deferred) {
// Now resort if required (don't resort if either told not to, or if 'at' option is set) and if there's more than one model
if (fillIn) {
options = options || {};
options['fillIn'] = true;
}
if (needSort && existingModel === undefined && !sort && at === undefined && collection._getLength() > 1) {
if (index > -1) {
cid = collection._getModel(index)['cid'];
}
var sortOpt = {};
oj.CollectionUtils.copyInto(sortOpt, options);
sortOpt['add'] = true;
collection.sort(sortOpt);
// Reset index--can't get it back if virtual--set to -1
if (index > -1) {
if (collection.IsVirtual()) {
index = -1;
}
else {
index = collection.indexOf(collection.getByCid(cid), deferred);
}
}
}
if (added) {
// Pass index property in options, if at is specified
if (options['at']) {
options['index'] = index;
}
if (newModel) {
newModel.TriggerInternal(silent, oj.Events.EventType['ADD'], newModel, collection, options);
addedModels.push(newModel);
}
else {
modelFoundInCollection.TriggerInternal(silent, oj.Events.EventType['ADD'], modelFoundInCollection, collection, options);
addedModels.push(modelFoundInCollection);
}
}
}
function mergeAttrs(collection, modelToTryAndMerge, modelFoundInCollection, newModel, deferred) {
var existingModel, modelAdded = null;
if (!force && merge && modelFoundInCollection) {
// Try to merge the attributes--we're merging and the model (by id) was already in the collection
needSort = modelFoundInCollection.Merge(modelToTryAndMerge, collection['comparator'], silent);
modelAdded = modelFoundInCollection;
}
else {
// Make sure model is not already in there
if (!force) {
existingModel = collection._getLocal(newModel);
if (existingModel && fillIn && at !== existingModel.index) {
// We're filling in a virtual collection: we should *not* be finding the new model already in the collection
// if we're not merging and not forcing: this indicates duplicate ids
//throw new Error("Duplicate IDs fetched or added without merging");
oj.Logger.warn("Duplicate ID fetched or added without merging, the id = " + existingModel.GetId());
}
}
if (existingModel === undefined) {
addToCollection(collection, newModel);
modelAdded = newModel;
}
else {
modelAdded = existingModel;
}
}
resortAndFireEvents(collection, existingModel, modelFoundInCollection, newModel, deferred);
return modelAdded;
}
function doAdd(collection, model, deferred) {
added = false;
var newModel = collection._newModel(model, true, options, false), modelToTryAndMerge = null, modelFoundInCollection = null;
if (newModel != null) {
index = -1;
// Make sure ID is up to date
newModel.SetupId();
// Use original model array not cloned model if merging--otherwise we won't find the model in the collection
modelToTryAndMerge = model instanceof oj.Model ? model : newModel;
if (deferred) {
if (force) {
return new Promise(function(resolve, reject) {
mergeAttrs(collection, modelToTryAndMerge, undefined, newModel, deferred);
resolve(undefined);
});
}
return collection._getInternal(modelToTryAndMerge, {'silent':true}, deferred, true).then(function (modInfo) {
modelFoundInCollection = modInfo['m'];
var mod = mergeAttrs(collection, modelToTryAndMerge, modelFoundInCollection, newModel, deferred);
modelReturnList.push(mod);
});
}
if (!force && merge) {
// Grab the actual model we want to merge from the collection, if the caller has indicated that we aren't
// forcing an add and we want to merge
modelFoundInCollection = fillIn ? collection._getLocal(modelToTryAndMerge) : collection.get(modelToTryAndMerge);
}
var modelAdded = mergeAttrs(collection, modelToTryAndMerge, modelFoundInCollection, newModel, deferred);
if (modelAdded) {
modelReturnList.push(modelAdded);
}
}
else {
// Add boolean falses for invalid models
modelReturnList.push(false);
}
}
function _parse(collection, array) {
// Must stop parsing if coming in from reset or constructor
if (collection.parse && options['parse'] && !options.noparse) {
return collection['parse'](array);
}
return array;
}
if (!fillIn && (this.IsVirtual() || deferred)) {
var self = this;
return new Promise(function(allResolve, allReject) {
var doTask = function(index) {
return new Promise(function(resolve, reject) {
doAdd(self, modelArray[index], true).then(function() {
resolve(index+1);
}, reject);
});
};
var currentStep = Promise.resolve(0);
modelArray = _parse(self, modelArray);
for (i = 0; i < modelArray.length; i++) {
currentStep = currentStep.then(doTask);
}
currentStep.then(function() {
if (addedModels.length > 0) {
self.TriggerInternal(options['silent'], oj.Events.EventType['ALLADDED'], self, addedModels, options);
}
allResolve(oj.Collection._returnModels(modelReturnList));
}, allReject);
});
}
modelArray = _parse(this, modelArray);
for (i = 0; i < modelArray.length; i++) {
doAdd(this, modelArray[i], false);
}
if (addedModels.length > 0) {
this.TriggerInternal(options['silent'], oj.Events.EventType['ALLADDED'], this, addedModels, options);
}
return oj.Collection._returnModels(modelReturnList);
};
oj.Collection._returnModels = function(modelReturnList) {
if (modelReturnList.length === 1) {
return modelReturnList[0];
}
return modelReturnList;
};
oj.Collection.prototype._hasComparator = function() {
return oj.Collection._defined(this['comparator']);
};
/**
* Sort the models in the collection. For virtual collections, any locally stored models are cleaned out in preparation for new fetches sending server-sorted models down to the client. No fetch is automatically performed.<p>
* For non-virtual collections, the models are sorted based on the comparator property.<p>
* See [comparator}{@link oj.Collection#comparator}<p>
* For virtual collections, all sorting must be handled by the server. If the string (attribute) comparator and sortDirection are set, then this information
* will be passed back via URL parameters, or passed to the customURL callback for application-construction of the appropriate URL. Function-based custom comparators are ignored in virtual collections.
* Events:<p>
* Fires a sort event, passing the collection and options<p>
* @param {Object=} options <b>silent</b>: if true, do not fire the sort event<br>
* <b>startIndex</b>: if provided, and if collection is virtual, do a fetch of startIndex+fetchSize immediately after the sort and return a promise. See [comparator}{@link oj.Collection#setRangeLocal}<p>
* @return {Promise|null} if virtual and if startIndex is provided in options, a promise Object that resolves upon completion. The promise will be passed an Object containing start and count properties that represent
* the *actual* starting position (start), count (count), and array (models) of the Models fetched, which may be fewer than what was requested. The promise will be rejected on
* an error and will pass the ajax status, xhr object, error, and collection, if relevant.
*
* @export
*/
oj.Collection.prototype.sort = function(options) {
options = options || {};
var silent = options['silent'], comparator = this['comparator'], self;
// Check for comparator
if (!this._hasComparator()) {
return null;
}
// This is a no-op in case of virtualization: we should just clear things out so that
// any elements will be refetched
if (this.IsVirtual()) {
var totalResults = this['totalResults'];
if (this._hasTotalResults()) {
// Make sure to set up the array if the length changes (i.e., from 0 to totalResults--need to preallocate)
this._setModels(new Array(totalResults), true);
}
else {
// No totalresults
this._setModels([], true);
this._resetLRU();
this._setLength();
}
var eventOpts = options['add'] ? {'add':true} : null;
this.TriggerInternal(silent, oj.Events.EventType['SORT'], this, eventOpts, null);
var startIndex = options['startIndex'];
if (startIndex !== undefined && startIndex !== null) {
return this.setRangeLocal(startIndex, this._getFetchSize(options));
}
return null;
}
self = this;
this._getModels().sort(function(a, b) {
return oj.Collection.SortFunc(a, b, comparator, self, self);
});
this._realignModelIndices(0);
// Indicate this sort is due to an add
var eventOpts = options['add'] ? {'add':true} : null;
this.TriggerInternal(silent, oj.Events.EventType['SORT'], this, eventOpts, null);
return null;
};
oj.Collection._getKey = function(val, attr) {
if (val instanceof oj.Model) {
return val.get(attr);
}
return oj.Model.GetPropValue(val, attr);
};
oj.Collection.SortFunc = function(a, b, comparator, collection, self) {
var keyA, keyB, i, retVal;
if ($.isFunction(comparator)) {
// How many args?
if (comparator.length === 1) {
// "sortBy" comparator option
keyA = comparator.call(self, a);
keyB = comparator.call(self, b);
var attrs1 = oj.StringUtils.isString(keyA) ? keyA.split(",") : [keyA];
var attrs2 = oj.StringUtils.isString(keyB) ? keyB.split(",") : [keyB];
for (i = 0; i < attrs1.length; i++) {
retVal = oj.Collection._compareKeys(attrs1[i], attrs2[i], collection['sortDirection']);
if (retVal !== 0) {
return retVal;
}
}
}
// "sort" comparator option
return comparator.call(self, a, b);
}
// String option
if (oj.StringUtils.isString(comparator)) {
var attrs = comparator.split(",");
for (i = 0; i < attrs.length; i++) {
keyA = oj.Collection._getKey(a, attrs[i]);
keyB = oj.Collection._getKey(b, attrs[i]);
retVal = oj.Collection._compareKeys(keyA, keyB, collection['sortDirection']);
if (retVal !== 0) {
return retVal;
}
}
}
return 0;
};
/**
* Return the index at which the given model would be inserted, using the collection comparator See [sort}{@link oj.Collection#sort}. Not supported for virtual collections.
*
* @param {Object} model model for which to determine the insert point
* @param {String|Object=} comparator optional comparator to override the default
* @returns {number} index at which model would be inserted. -1 if no comparator
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.sortedIndex = function(model, comparator) {
var comp = comparator ? comparator : this['comparator'], self, test;
// Check for comparator
if (!comp) {
return -1;
}
this._throwErrIfVirtual("sortedIndex");
self = this;
test = function(a, b) {
var keyA, keyB;
if ($.isFunction(comp)) {
// How many args?
if (comp.length === 1) {
// "sortBy" comparator option
keyA = comp.call(self, a);
keyB = comp.call(self, b);
var attrs1 = oj.StringUtils.isString(keyA) ? keyA.split(",") : [keyA];
var attrs2 = oj.StringUtils.isString(keyB) ? keyB.split(",") : [keyB];
var retVal, i;
for (i = 0; i < attrs1.length; i++) {
retVal = oj.Collection._compareKeys(attrs1[i], attrs2[i], self['sortDirection']);
if (retVal !== 0) {
return retVal;
}
}
}
// "sort" comparator option
return comp.call(self, a, b);
}
// String option
if (oj.StringUtils.isString(comp)) {
keyA = a.get(comp);
keyB = b.get(comp);
return oj.Collection._compareKeys(keyA, keyB, self['sortDirection']);
}
return 0;
};
return oj.Collection._find(this._getModels(), model, test);
};
// Binary search and return the index at which model would be inserted into sorted modelArray
oj.Collection._find = function(modelArray, model, comparator) {
function search(min, max) {
var cid, id, mid;
if (min > max) {
return -1;
}
cid = model.GetCid();
id = model.GetId();
if (modelArray[min].Match(id, cid)) {
return min;
}
if (modelArray[max].Match(id, cid)) {
return max;
}
mid = Math.floor((max+min)/2);
if (comparator(modelArray[mid], model) === -1) {
return search(min+1, mid);
}
if (comparator(modelArray[mid], model) === 1) {
return search(mid, max-1);
}
return mid;
}
return search(0, modelArray.length-1);
};
oj.Collection._compareKeys = function(keyA, keyB, sortDirection) {
if (sortDirection === -1) {
if (keyA < keyB) {
return 1;
}
if (keyB < keyA) {
return -1;
}
}
else {
if (keyA > keyB) {
return 1;
}
if (keyB > keyA) {
return -1;
}
}
return 0;
};
/**
* Add the given model to the front of the collection<br>
* For events that may be fired, see [add]{@link oj.Collection#add}.<br>
* @param {Object} m model to add to the beginning of the collection
* @param {Object=} options See [add]{@link oj.Collection#add}
* @return {Promise} If this is a virtual collection, this will return a promise that will be resolved when the
* operation is done. Otherwise undefined
* @export
*/
oj.Collection.prototype.unshift = function(m, options) {
// Like an add but set 'at' to zero if not specified
var opt = {};
oj.CollectionUtils.copyInto(opt, options || {});
if (!opt['at']) {
opt['at'] = 0;
}
this._manageLRU(1);
return this._handlePromise(this._addInternal(m, opt, false, opt['deferred']));
};
oj.Collection.prototype._handlePromise = function(result) {
if ($.isFunction(result.then)) {
return this._addPromise(function() {
return result;
});
}
return result;
};
/**
* Remove the first model from the collection and return it.<br>
* For events that may be fired, see [remove]{@link oj.Collection#remove}.<br>
* @param {Object=} options same as remove, plus: <br>
* <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
* @return {Promise} model that was removed. If this is a virtual collection, this will return a promise
* which will resolve passing the model value that was removed
* @export
*/
oj.Collection.prototype.shift = function(options) {
var deferred = this._getDeferred(options);
if (this.IsVirtual() || deferred) {
var self = this;
return this.at(0, options).then(function (model) {
return self._removeInternal(model, 0, options);
});
}
return this._removeInternal(this.at(0), 0, options);
};
/**
* @export
* @desc Return an array of models found in the Collection, excepting the last n.
* @param {number=} n number of models to leave off the returned array; defaults to 1
* @return {Array} array of models from 0 to the length of the collection - n - 1
* @throws {Error} when called on a virtual collection
*/
oj.Collection.prototype.initial = function(n) {
if (n === undefined) {
n = 1;
}
this._throwErrIfVirtual("initial");
var array = [], i;
for (i = 0; i < this._getLength() - n; i=i+1) {
array.push(this.at(i));
}
return array;
};
oj.Collection.prototype._getDeferred = function(options) {
var opt = options || {};
return opt['deferred'];
};
/**
* Return the last model in the collection. If n is passed in, then the last n models are returned as an array
* Note that if the collection is virtual, and totalResults is not returned by the server, the results returned by last can be
* difficult to predict. They depend on the fetch sizes, last known offset of a fetch, etc. If code is using a server that does not return
* totalResults the use of last is not recommended.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
* @param {number=} n number of models to return. Defaults to 1
* @param {Object=} options <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
* @return {Promise|Object|null} array of n models from the end of the Collection. If this is a virtual collection,
* this will return a promise which will resolve passing the array or single model
* @export
*/
oj.Collection.prototype.last = function(n, options) {
var deferred = this._getDeferred(options);
if (n === undefined) {
n = 1;
}
if (n === 1) {
var len = this._getModelsLength();
if (len === 0) {
len = n;
}
if (len > 0) {
return this.at(len-1, deferred);
}
return null;
}
var array = [], i;
var len = this._getLength();
if (deferred || this.IsVirtual()) {
// Loop using deferred
var start = len-n;
// Handle edge or no totalResults cases
if (start < 0) {
start = 0;
}
if (len === 0) {
// No totalresults, probably
len = n;
}
return this._addPromise(function() {
return this.IterativeAt(start, len);
});
}
for (i = len - n; i < len; i=i+1) {
array.push(this.at(i));
}
return array;
};
// Loop calling at() in a deferred way and return a promise to be resolved when all the elements are sequentially fetched
oj.Collection.prototype.IterativeAt = function (start, end) {
var array = [], i;
var self = this;
return new Promise(function(allResolve, allReject) {
var doTask = function(index) {
return new Promise(function(resolve, reject) {
self._deferredAt(index, null).then(function(model) {
array.push(model);
resolve(index+1);
}, reject);
});
};
var currentStep = Promise.resolve(start);
for (i = start; i < end; i++) {
currentStep = currentStep.then(doTask);
}
currentStep.then(function() {
allResolve(array);
}, allReject);
});
};
oj.Collection.prototype._getDefaultFetchSize = function(n) {
if (n === undefined || n === null) {
return this[oj.Collection._FETCH_SIZE_PROP];
}
else {
return n;
}
};
oj.Collection.prototype._calculateNextStart = function() {
var lastFetch = this['lastFetchCount'];
if (lastFetch === undefined || lastFetch === null) {
lastFetch = this[oj.Collection._FETCH_SIZE_PROP];
}
if (this['offset'] === undefined || this['offset'] === null) {
// Assume zero offset (0+lastFetch)
return lastFetch;
}
return this['offset'] + lastFetch;
};
/**
* Fetch the next set of models from the server.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
*
* @param {number} n number of models to fetch. If undefined or null, the collection will attempt to use the overall [fetchSize]{@link oj.Collection#fetchSize} property value
* @param {Object=} options Options to control next<p>
* <b>success</b>: a user callback called when the fetch has completed successfully. This makes the fetch an asynchronous process. The callback is called passing the Collection object, raw response, and the options argument.<br>
* <b>error</b>: a user callback function called if the fetch fails. The callback is called passing the collection object, xhr, and options arguments.<br>
* @return {Object} xhr ajax object, by default. null if nothing to fetch (the success callback will still be called). Note if [sync]{@link oj.Collection#sync} has been replaced, this would be the value returned by the custom implementation.
* @export
*/
oj.Collection.prototype.next = function(n, options) {
options = options || {};
options[oj.Collection._FETCH_SIZE_PROP] = this._getDefaultFetchSize(n);
var start = this._calculateNextStart();
var length = this._getLength();
if (length === 0 && options[oj.Collection._FETCH_SIZE_PROP] > 0) {
// If we have a fetch size and we have no length let next() do a fetchSize fetch starting at zero to kick things off
start = 0;
} else if (start >= length) {
// No op -- still call success because the items are already fetched.
var self = this;
if (options['success']) {
options['success'].call(oj.Model.GetContext(options, self), self, null, options);
}
return null;
}
options['startIndex'] = start;
return this.fetch(options);
};
oj.Collection.prototype._calculatePrevStart = function(n) {
if (this['offset'] === undefined || this['offset'] === null) {
// Assume zero: we can't back up beyond that so if the offset wasn't set there's nothing to do
return 0;
}
return this['offset'] - n;
};
/**
* @export
* Fetch the previous set of models from the server.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
* @param {number} n number of models to fetch. If undefined or null, the collection will attempt to use the overall fetchSize property value
* @param {Object=} options Options to control previous<p>
* <b>success</b>: a user callback called when the fetch has completed successfully. This makes the fetch an asynchronous process. The callback is called passing the Collection object, raw response, and the options argument.<p>
* <b>error</b>: a user callback function called if the fetch fails. The callback is called passing the collection object, xhr, and options arguments.<p>
* @return {Object} xhr ajax object, by default. null if there is nothing earlier to fetch (no fetch has happened or the last fetch started at 0). The success callback will still be called. Note if [sync]{@link oj.Collection#sync} has been replaced, this would be the value returned by the custom implementation.
*/
oj.Collection.prototype.previous = function(n, options) {
options = options || {};
if (this['offset'] === 0) {
// No op -- still call success (if we've fetched before--lastFetchCount is other than zero) because the items are already fetched.
var self = this;
if (options['success'] && this['lastFetchCount']) {
options['success'].call(oj.Model.GetContext(options, self), self, null, options);
}
return null;
}
options[oj.Collection._FETCH_SIZE_PROP] = this._getDefaultFetchSize(n);
var start = this._calculatePrevStart(options[oj.Collection._FETCH_SIZE_PROP]);
if (start < 0) {
// Only fetch from 0 to the last fetch's starting point...
options[oj.Collection._FETCH_SIZE_PROP] = this['offset'];
start = 0;
}
options['startIndex'] = start;
return this.fetch(options);
};
/**
* Set or change the number of models held at any one time
*
* @param {number} n maximum number of models to keep at a time
* @export
*/
oj.Collection.prototype.setModelLimit = function(n) {
this['modelLimit'] = n;
// Clean out down to the new limit, if necessary
this._manageLRU(0);
};
oj.Collection.prototype._getModelLimit = function() {
return this['modelLimit'];
};
/**
* Set or change the number of models to fetch with each server request
*
* @param {number} n number of models to fetch with each request
* @export
*/
oj.Collection.prototype.setFetchSize = function(n) {
this[oj.Collection._FETCH_SIZE_PROP] = n;
};
/**
* Return the array of models found in the Collection starting with index n.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
* @param {number=} n index at which to start the returned array of models. Defaults to 1.
* @param {Object=} options <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
* @return {Array|Promise} array of models from the collection. If this is a virtual collection, or
* if deferred is passed as true, return a promise which resovles passing the array of models.
* @export
*/
oj.Collection.prototype.rest = function(n, options) {
var deferred = this._getDeferred(options);
if (n === undefined) {
n = 1;
}
var array = [], i;
// TODO
if (this.IsVirtual() || deferred) {
return this._addPromise(function() {
return this.IterativeAt(n, this._getLength());
});
}
for (i = n; i < this._getLength(); i=i+1) {
array.push(this.at(i));
}
return array;
};
/**
* Remove a model from the collection, if found.<br>
* Events:<p>
* <ul>
* <b>remove</b>: fired for each model removed, passing the model removed, collection, and options<br>
* <b>allremoved</b>: fired after all models have been removed, passing the collection, array of models removed, and options<br>
* </ul>
* <p>
* @param {Object|Array} m model object or array of models to remove.
* @param {Object=} options <b>silent</b>: if set, do not fire events
* @return {Array|Object} an array of models or single model removed
* @export
*/
oj.Collection.prototype.remove = function(m, options)
{
options = options || {};
var modArray = [], mod;
if (m instanceof Array) {
modArray = m;
}
else {
modArray.push(m);
}
var modsRemoved = [], model;
for (mod = modArray.length-1; mod >= 0; mod=mod-1) {
// Done to keep array in order matching one passed in--we do removal in reverse
modsRemoved.unshift(this._removeInternal(modArray[mod], -1, options));
}
this.TriggerInternal(options['silent'], oj.Events.EventType['ALLREMOVED'], this, modArray, options);
return oj.Collection._returnModels(modsRemoved);
};
oj.Collection.prototype._removeInternal = function(model, index, options) {
options = options || {};
var modInfo = index == -1 ? this._getInternal(model) : oj.Collection._getModinfo(index, model),
silent = options['silent'];
index = modInfo.index;
if (index > -1) {
var mod = modInfo['m'];
// only unset the collection setting if it's mine
if (mod !== undefined && mod.GetCollection() === this) {
mod.SetCollection(null);
}
this._spliceModels(index, 1);
this._setLength();
//if (!silent) {
var opt = {};
oj.CollectionUtils.copyInto(opt, options);
opt['index'] = index;
if (mod !== undefined) {
mod.TriggerInternal(silent, oj.Events.EventType['REMOVE'], mod, this, opt);
}
// }
// Unlisten after event fired
this._unlistenToModel(mod);
}
return modInfo['m'];
};
oj.Collection.prototype._unlistenToModel = function(m) {
if (m !== undefined) {
m.off(null, null, this);
}
};
oj.Collection.prototype._listenToModel = function(m) {
m.OnInternal(oj.Events.EventType['ALL'], this._modelEvent, this, false, true);
};
// Handle model destroyed events via the all listener
oj.Collection.prototype._modelEvent = function(event, model, collection, options) {
var args;
if (event === oj.Events.EventType['DESTROY']) {
this.remove(model);
}
// Don't process general events if we're not the target
if (collection !== undefined && collection instanceof oj.Collection && collection !== this) {
return;
}
// Throw up to the collection
args = Array.prototype.slice.call(arguments);
var silent = options && options['silent'];
this.TriggerInternal(silent, event, model, collection, options);
};
/**
* Clear all data from the collection and refetch (if non-virtual). If virtual, clear all data.
* If fetch() cannot be called (e.g., there is no URL) then the caller must reload the collection wtih data.
* The existing data is still cleared.
* Events (if silent is not set):<p>
* <ul>
* <b>refresh</b>: fired passing the collection and options<br>
* For more events that may be fired if the collection is not virtual, see [fetch]{@link oj.Collection#fetch}.
* </ul>
* <p>
* @param {Object=} options user options<p>
* <b>silent</b>: if set, do not fire a refresh event<p>
* <b>startIndex</b>: if provided, and if collection is virtual, do a fetch of startIndex+fetchSize immediately after the refresh and return a promise. See [comparator}{@link oj.Collection#setRangeLocal}<p>
* @return {Promise} promise object resolved when complete (in case there is a fetch for non-virtual mode). If startIndex is provided as an option, the returned promise resolution is the same as setRangeLocal.
* @export
*/
oj.Collection.prototype.refresh = function(options)
{
options = options || {};
var self = this;
return this._addPromise(function() {
return new Promise(function(resolve, reject) {
if (!self.IsVirtual()) {
var silent = options['silent'] !== undefined && options['silent'];
try {
// Do a reset, with silent
self.reset(null, {'silent':true});
// Local: do a fetch to fill back up
// In case options are passed to refresh-->fetch
var opt = {};
for (var prop in options) {
if (options.hasOwnProperty(prop)) {
opt[prop] = options[prop];
}
}
opt['success'] = function (collection, response, options) {
self.TriggerInternal(silent, oj.Events.EventType['REFRESH'], self, options, null);
resolve({'collection':collection, 'response':response, 'options':options});
};
opt['error'] = function (xhr, status, error) {
reject(oj.Collection._createRejectionError(xhr, status, error, self, options));
};
self.fetch(opt);
return;
}
catch (e) {
// This is OK if it's a URLError: just fire the event: local collection without custom sync
if (e instanceof oj.URLError) {
// if it's a completely local collection, it's a no-op other than the event
self.TriggerInternal(silent, oj.Events.EventType['REFRESH'], self, options, null);
resolve({'collection':self, 'options':options});
return;
}
throw e;
}
}
// Virtual
//var totalResults = self['totalResults'];
var startIndex = options['startIndex'];
/* if (oj.Collection._defined(totalResults)) {
self._setModels(new Array(totalResults), true);
self._resetLRU();
}*/
self._setModels([], true);
self._resetLRU();
// Clear totalresults.
self['totalResults'] = undefined;
self._setLength();
var silent = options['silent'] !== undefined && options['silent'];
self.TriggerInternal(silent, oj.Events.EventType['REFRESH'], self, options, null);
if (startIndex === undefined || startIndex === null) {
startIndex = 0;
}
if (startIndex !== undefined && startIndex !== null) {
// Do a set range local
self._setRangeLocalInternal(startIndex, self._getFetchSize(options)).then(function(actual) {
resolve(actual);
});
}
else {
resolve(undefined);
}
});
});
};
/**
* Remove and replace the collection's entire list of models with a new set of models, if provided. Otherwise, empty the collection. totalResults is reset when no new data is passed in, set to the new length in the non-virtual case if data is passed in, and left as was in the virtual case if data is passed in.<br>
* Events (if silent is not set):<p>
* <ul>
* <b>reset</b>: fired passing the collection and options. The new length of the collection should be in effect when the event is fired<br>
* For events that may be fired if data is passed in, see [add]{@link oj.Collection#add}.
* </ul>
* <p>
* @param {Object=} data Array of model objects or attribute/value pair objects with which to replace the collection's data.
* @param {Object=} options user options, passed to event, unless silent<br>
* <b>silent</b>: if set, do not fire events<p>
* @returns {Object|Array} The model or models added to the collection, if passed in
* @export
*/
oj.Collection.prototype.reset = function(data, options)
{
var opts = {};
oj.CollectionUtils.copyInto(opts, options || {});
opts['previousModels'] = this._getModels();
var index, model;
for (var i = 0; i < this._modelIndices.length; i++) {
index = this._modelIndices[i];
model = this._getModel(index);
if (model) {
this._unlistenToModel(model);
model.SetCollection(null);
}
}
this._setModels([], true);
this._resetLRU();
var silent = opts['silent'] !== undefined && opts['silent'];
if (!data) {
// Clear totalresults. If not virtual, _setLength will reset it to the length
this['totalResults'] = undefined;
this._setLength();
this.TriggerInternal(silent, oj.Events.EventType['RESET'], this, opts, null);
return null;
}
var retObj = null;
// Parse collection
if (opts['parse']) {
data = this['parse'](data);
}
this._manageLRU((data instanceof Array) ? data.length : 1);
opts.noparse = true;
retObj = this._addInternal(data, opts, true, false);
this._setLength();
this.TriggerInternal(silent, oj.Events.EventType['RESET'], this, opts, null);
return this._handlePromise(retObj);
};
/**
* Return the model object found at the given index of the collection, or a promise object that will pass the model as an argument when it resolves.<p>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.
* @param {number} index Index for which to return the model object.
* @param {Object=} options <b>fetchSize</b>: fetch size to use if the call needs to fetch more records from the server, if virtualized. Overrides the overall fetchSize setting <br>
* <b>deferred</b>: if true, return a deferred/promise object as described below. If not specified, the type of return value will be determined by whether or not the collection is virtual<br>
*
* @return {Object|null} Model [model]{@link oj.Model} located at index. If index is out of range, returns null. If this is a virtual collection, or
* if deferred is specified and true, at will return a Promise object which will resolve passing the model at the given index
* @export
*/
oj.Collection.prototype.at = function(index, options)
{
var deferred = this._getDeferred(options);
return this._atInternal(index, options, false, deferred);
};
// Local indicates that only what's stored locally should be returned (if true)--no fetching
oj.Collection.prototype._atInternal = function(index, options, local, deferred) {
if (index < 0) {
// Normalize it using the length-- another BackboneJS test case
index += this._getLength();
}
if (index < 0 || this._overUpperLimit(index)) {
return null;
}
if (!local && (this.IsVirtual() || deferred)) {
return this._addPromise(function() {
return this._deferredAt(index, options);
});
}
return this._getModel(index);
};
/**
* Return a promise, the resolution of which indicates that all promise-returning API in process have completed.
*
* @returns {Promise} a promise that when resolved, indicates that all unresolved promise-returning API calls made at the moment of the whenReady have completed
* @export
*/
oj.Collection.prototype.whenReady = function() {
if (this._promises) {
// If we have an active chain, return it
return this._promises;
}
// Otherwise return an immediately resolved promise that means nothing...
return Promise.resolve();
};
// Add a task to a chained list of Promises
oj.Collection.prototype._addPromise = function(promiseTask) {
var self = this;
// Undefined, so set it up initially
if (this._promises === undefined)
{
this._promiseCount = 0;
this._promises = Promise.resolve();
}
// Track the number we have left to resolve
this._promiseCount++;
// Chain this new promise callback task to the end of the list
this._promises = this._promises.then(promiseTask.bind(self)).then(
function(arg)
{
// Resolved successfully--decrement our count and clean up if we have none left to resolve
self._promiseCount--;
if (self._promiseCount === 0)
{
self._promises = undefined;
// Fire the ready event
self.TriggerInternal(false, oj.Events.EventType['READY'], self, null, null);
}
// Resolve the true promise with the value we're given
return arg;
},
function(error)
{
// Rejected--decrement our count and clean up if we have none left to resolve
self._promiseCount--;
if (self._promiseCount === 0)
{
self._promises = undefined;
}
// Reject the promise
return Promise.reject(error);
});
// Return the chain with the new promise at the end
return this._promises;
};
oj.Collection.prototype._deferredAt = function(index, options) {
var self = this;
// If it's virtual, we need to see if this item has been fetched or not: if not, we need to fetch it + fetchSize
var model = self._getModel(index);
if (model === undefined) {
return new Promise(function(resolve, reject) {
// Go fetch
var opts = {};
oj.CollectionUtils.copyInto(opts, options || {});
opts['context'] = self;
opts['startIndex'] = index;
opts['error'] = function(xhr, status, error) {
// Handle potential errors
reject(oj.Collection._createRejectionError(xhr, status, error, self, options));
return;
}
opts['success'] = function(collection, response, retOpt) {
resolve(self._getModel(index));
}
self._fetchInternal(opts, -1, false);
/*self._fetchInternal(opts, -1, false).then(function () {
resolve(self._getModel(index));
}, function(e) { reject(e);});*/
});
}
return new Promise(function(resolve, reject) {
resolve(model);
});
};
/**
* Return the first model object from the collection whose client ID is the given model cid
* @param {string} clientId Client ID (see Model cid) for which to return the model object, if found.
* @return {Object|null} First model object in the collection where model.cid = clientId. If none are found, returns null.
*
* @throws {Error} when called on a virtual Collection if the item isn't found in memory
* @export
*/
oj.Collection.prototype.getByCid = function(clientId)
{
var models = this._getModels();
var index;
var foundModel = null;
for (var i = 0; i < this._modelIndices.length; i++) {
index = this._modelIndices[i];
if (models[index] && clientId === models[index]['cid']) {
foundModel = models[index];
break;
}
}
if (foundModel) {
return foundModel;
}
if (this.IsVirtual()) {
throw new Error("Not found locally and not supported by server for virtual collections");
}
return null;
};
/**
* Return the first model object from the collection whose model id value is the given id or cid, or the id or cid from a passed in model
* Note this method will not function as expected if the id or cid is not set<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
*
* @param {Object|string} id ID, cid, or Model (see Model id or cid) for which to return the model object, if found.
* @param {Object=} options <p>
* <b>fetchSize</b>: fetch size to use if the call needs to fetch more records from the server, if virtualized. Overrides the overall fetchSize setting<p>
* <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
* @return {Object|null|Promise} First model object in the collection where model.id = id or model.cid = id. If none are found, returns null.
* If deferred or virtual, return a promise passing the model when done
* @export
*/
oj.Collection.prototype.get = function(id, options)
{
var deferred = this._getDeferred(options);
var internalGet = this._getInternal(id, options, deferred);
if (internalGet) {
// Is this a deferred object?
if ($.isFunction(internalGet.then)) {
return this._addPromise(function() {
return new Promise(function(resolve, reject) {
internalGet.then(function(modInfo) {
resolve(modInfo['m']);
},
function (err) {
reject(err);
});
});
});
}
if (this.IsVirtual()) {
return this._addPromise(function() {
return new Promise(function(resolve, reject) {
resolve(internalGet['m']);
});
});
}
if (internalGet.hasOwnProperty('m')) {
return internalGet['m'];
}
}
return null;
};
oj.Collection.prototype._getLocal = function(id) {
var internalGet = this._getLocalInternal(id);
if (internalGet) {
return internalGet['m'];
}
return null;
};
oj.Collection.prototype._getLocalInternal = function(id) {
var cid = id;
if (id instanceof oj.Model) {
// Get the cid
cid = id.GetCid();
// Get the id
id = id.GetId();
}
else if (oj.Collection._defined(id) && id['id'] !== undefined) {
id = id['id'];
}
var foundObj = null;
var len = this._modelIndices.length;
var model;
var models = this._getModels();
var index;
for (var i = 0; i < len; i++) {
index = this._modelIndices[i];
model = models[index];
if (model !== undefined && model.Match(id, cid)) {
foundObj = oj.Collection._getModinfo(index, model);
break;
}
}
if (foundObj) {
return foundObj;
}
return oj.Collection._getModinfo(-1, undefined);
};
/**
* @private
* @param {Object|null|string} id
* @param {Object=} options
* @param {boolean=} deferred
* @param {boolean=} fillIn
* @returns {Object}
*/
oj.Collection.prototype._getInternal = function(id, options, deferred, fillIn) {
var cid = id;
if (fillIn === undefined) {
fillIn = false;
}
if (id instanceof oj.Model) {
// Get the cid
cid = id.GetCid();
// Get the id
id = id.GetId();
}
else if (oj.Collection._defined(id) && id['id'] !== undefined) {
id = id['id'];
}
var foundObj = null;
var models = this._getModels();
var index;
for (var i = 0; i < this._modelIndices.length; i++) {
index = this._modelIndices[i];
if (models[index] && models[index].Match(id, cid)) {
var retObj = oj.Collection._getModinfo(index, models[index]);
foundObj = retObj;
break;
}
};
if (foundObj) {
if (deferred) {
return new Promise(function(resolve, reject) {
resolve(foundObj);
});
}
return foundObj;
}
// If virtual, might be undefined because it needs to be fetched
if (this.IsVirtual()) {
// Try to fetch using start ID. cid not supported
if (id === undefined && cid !== undefined) {
return new Promise(function(resolve, reject) {
resolve(oj.Collection._getModinfo(-1, undefined));
});
}
var self = this;
return new Promise(function(resolve, reject) {
var resp = function (resp) {
if (resp != null) {
var index = self._getOffset();
// Check that the model at index is the right one
var model = self._getModel(index);
if (model !== undefined && model.Match(id, cid)) {
resolve(oj.Collection._getModinfo(index, model));
}
else {
resolve(oj.Collection._getModinfo(-1, undefined));
}
}
else {
resolve(oj.Collection._getModinfo(-1, undefined));
}
};
// Go fetch
var opts = {};
oj.CollectionUtils.copyInto(opts, options || {});
opts['context'] = self;
opts['startID'] = id;
opts['error'] = function(xhr, status, error) {
// Handle potential errors
reject(oj.Collection._createRejectionError(xhr, status, error, self, options));
}
opts['success'] = function(collection, response, retOpts) {
resp(response);
}
self._fetchInternal(opts, -1, fillIn);
});
}
var undefinedModInfo = oj.Collection._getModinfo(-1, undefined);
if (deferred) {
return new Promise(function(resolve, reject) {
resolve(undefinedModInfo);
});
}
return undefinedModInfo;
};
oj.Collection._getModinfo = function(index, model) {
return {index: index, 'm': model};
};
oj.Collection.prototype['parse'] = function (response) {
var prop;
// Try to interpret ADFbc like controls where a collection is hanging off a property
if (response instanceof Array) {
return response;
}
if (!response) {
return response;
}
// See if any of the properties contain arrays
for (prop in response) {
if (response.hasOwnProperty(prop)) {
if (response[prop] instanceof Array) {
return response[prop];
}
}
}
return response;
};
// Determine if actual means we are complete
oj.Collection.prototype._checkActual = function(start, count, actual) {
// Are we at the end with what actually came back? Then this request should satisfy the setLocalRange
if (this._hasTotalResults() && this['totalResults'] > 0 && (actual.start + actual.count >= this['totalResults'])) {
return true;
}
return (actual.start === start && actual.count === count);
};
/**
*
* Tell the collection to try and ensure that the given range is available locally. Note that if the collection is virtual, setRangeLocal may make repeated fetch calls to the server to satisfy the request, stopping only when the range has been made local or the end of the records on the server is reached.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
*
* @param {number} start starting index to make local
* @param {number} count number of elements to make local
* @return {Promise} a promise Object that resolves upon completion. The promise will be passed an Object containing start and count properties that represent
* the *actual* starting position (start), count (count), and array (models) of the Models fetched, which may be fewer than what was requested. The promise will be rejected on
* an error and will pass the ajax status, xhr object, error, and collection, if relevant.
* @export
*/
oj.Collection.prototype.setRangeLocal = function(start, count) {
var self = this;
return this._addPromise(function() {
return self._setRangeLocalInternal(start,count);
});
};
oj.Collection.prototype._setRangeLocalInternal = function(start, count) {
if (this.IsVirtual()) {
// make sure we reconcile the length to what we think the totalresults are--if there have been any non fetched changes in
// length we don't want to be placing things wrong
this._resetModelsToFullLength();
}
var actual = this._getLocalRange(start, count);
var self = this;
if (this._checkActual(start, count, actual)) {
return new Promise(function (resolve, reject) {
resolve(actual);
});
}
// Manage the LRU - set model limit at least as high as the count we're trying to fetch
var modelLimit = this._getModelLimit();
if (modelLimit > -1 && modelLimit < count) {
this['modelLimit'] = count;
}
return new Promise(function (resolve, reject) {
self._setRangeLocalFetch(start, count, -1, {start:start, count:count}, resolve, reject, true);
});
}
oj.Collection.prototype._setRangeLocalFetch = function(start, count, placement, original, resolve, reject, fill) {
var self = this;
var resp = function () {
var actual = self._getLocalRange(original.start, original.count);
if (fill && self._hasTotalResults() && actual.count < original.count) {
// The range wasn't fulfilled: try again
var newPlacement = actual.start + actual.count;
// Try the next block...don't repeat the request
var newStart = start + count;
if (newStart < self['totalResults']) {
self._setRangeLocalFetch(newStart, count, newPlacement, original, resolve, reject, fill);
}
else {
// Can't go any further
resolve(actual);
}
}
else {
resolve(actual);
}
};
// Go fetch
var limit = start + count;
// Get the greater of the limit-start or fetchSize
if (this[oj.Collection._FETCH_SIZE_PROP] && this[oj.Collection._FETCH_SIZE_PROP] > count) {
limit = this[oj.Collection._FETCH_SIZE_PROP] + start;
}
// Now, to optimize, move start up to the first undefined model in the sequence
var opts = null;
if (this.IsVirtual()) {
var newStart = this._getFirstMissingModel(start, limit);
if (newStart > start) {
start = newStart;
// Recheck the limit
limit = start + count;
// Get the greater of the limit-start or fetchSize
if (this[oj.Collection._FETCH_SIZE_PROP] && this[oj.Collection._FETCH_SIZE_PROP] > count) {
limit = this[oj.Collection._FETCH_SIZE_PROP] + start;
}
}
opts = {'context': this, 'startIndex': start, 'fetchSize': limit-start};
}
else {
opts = {'context': this};
}
opts['error'] = function(xhr, status, error) {
// Handle potential errors
reject(oj.Collection._createRejectionError(xhr, status, error, self, null));
return;
}
opts['success'] = function(collection, response, retOpts) {
resp();
}
try {
this._fetchInternal(opts, placement, placement > -1);
}
catch (e) {
// This is OK if it's a URLError: local collection with no means of fetching: just resolve
if (e instanceof oj.URLError) {
var actual = self._getLocalRange(start, count);
resolve(actual);
}
}
};
oj.Collection._createRejectionError = function(xhr, status, error, collection, options) {
var silent = false;
if (options && options['silent']) {
silent = options['silent'];
}
oj.Model._triggerError(collection, silent, options, status, error, xhr);
var err = new Error(status);
err['xhr'] = xhr;
err['error'] = error;
err['collection'] = collection;
err['status'] = status;
return err;
};
oj.Collection.prototype._getMaxLength = function(start, count) {
var len = this._getModelsLength();
if (len === 0) {
// This is an exception: could be uninitialized
return start + count;
}
return start + count > len ? len : start + count;
};
/**
* Determine if every element of the given range is filled in locally
*
* @param {number} start starting index to check
* @param {number} count number of elements to check
* @return {boolean} true if all elements are local, false otherwise
* @export
*/
oj.Collection.prototype.isRangeLocal = function(start, count) {
var localRange = this._getLocalRange(start, count);
if (this._getModelsLength() === 0) {
// If we have no models length, then range cannot be thought of as local unless the count is zero--edge case
return count === 0;
}
return start === localRange.start && (count === localRange.count || start+count > this._getModelsLength());
};
oj.Collection.prototype._getModelArray = function(start, count) {
var retArr = [];
var models = this._getModels();
var end = start + count;
for (var i = start; i < end ; i++) {
retArr.push(models[i]);
}
return retArr;
};
// For a given range of models, return the actual range which are local within that set.
oj.Collection.prototype._getLocalRange = function(start, count) {
// Not virtual, local if there are any models
if (!this.IsVirtual()) {
if (this._getModelsLength() > 0) {
if (start+count > this._getModelsLength()) {
// Over the boundary
var c = this._getModelsLength()-start;
return {'start':start, 'count':c, 'models':this._getModelArray(start, c)};
}
return {'start':start, 'count':count, 'models':this._getModelArray(start, count)};
}
return {'start':start, 'count':0, 'models':[]};
}
var limit = this._getMaxLength(start, count);
// Adjust for no totalResults
if (!this._hasTotalResults() && limit < start + count) {
// We don't know if it's local or not
return {'start':start, 'count':(limit-start), 'models':this._getModelArray(start, limit-start)};
}
if (limit === 0) {
// There nothing here
return {'start':start, 'count':0, 'models':[]};
}
var firstMissingModel = this._getFirstMissingModel(start, limit);
if (firstMissingModel > -1) {
return {'start':start, 'count':(firstMissingModel-start),'models':this._getModelArray(start, firstMissingModel-start)};
}
// Make sure start doesn't overrun the end!
if (start > limit) {
count = 0;
} else {
// Make sure that start+count doesn't overrun the end
if (start+count > limit) {
count = limit-start;
}
}
return {'start':start,'count':count,'models':this._getModelArray(start, count)};
};
// Return the first model between start and limit that's undefined
oj.Collection.prototype._getFirstMissingModel = function(start, limit) {
for (var i = start; i < limit; i++) {
if (this._getModel(i) === undefined) {
return i;
}
}
return -1;
};
/**
* Loads the Collection object from the data service URL. Performs a data "read."<p>
* Events:<p>
* <ul>
* <b>request</b>: fired when the request to fetch is going to the server, passing the collection, xhr object, and options<br>
* <b>sync</b>: fired when the collection is fetched from the data service, passing the collection and the raw response<br>
* <b>error</b>: fired if there is an error during the fetch, passing the collection, xhr object, options<br>
* </ul>
* <p>
* @param {Object=} options Options to control fetch<p>
* <b>success</b>: a user callback called when the fetch has completed successfully. This makes the fetch an asynchronous process. The callback is called passing the Collection object, raw response, and the fetch options argument.<br>
* <b>error</b>: a user callback function called if the fetch fails. The callback is called passing the collection object, xhr, and options arguments.<br>
* <b>add</b>: if set, new records retrieved from the data service will be added to those models already in the collection. If not set, the records retrieved will be passed to the reset() method, effectively replacing the previous contents of the collection with the new data. Not supported for virtual/paging cases.<br>
* <b>set</b>: if true, fetch will try to use the set function to try and merge the fetched models with those already in the collection, on the client. The default behavior is to reset the collection before adding back in the fetched models. This default is the reverse of Backbone's current default.<br>
* <b>startIndex</b>: numeric index with which to start fetching Models from the server. The page setting controls the number of Models to be fetched. startID takes precedence over startIndex if both are specified. If both are specified and startID isn't supported then startIndex will be used instead.<br>
* <b>startID</b>: unique ID of the Model to start fetching from the server. The page setting controls the number of Models to be fetched. Note if this is not supported by the server then startID will be ignored.<br>
* <b>since</b>: fetch records having a timestamp since the given UTC time<br>
* <b>until</b>: fetch records having a timestamp up to the given UTC time<br>
* <b>fetchSize</b>: use specified page size instead of collection's setting<br>
* @return {Object} xhr ajax object, by default. If [sync]{@link oj.Collection#sync} has been replaced, this would be the value returned by the custom implementation.
* @export
*/
oj.Collection.prototype.fetch = function (options) {
return this._fetchInternal(options, -1, false);
};
// fillIn is used to indicate that this fetch is just the result of a get() or part of an add(), etc., when virtual
oj.Collection.prototype._fetchInternal = function(options, placement, fillIn) {
function doReset(collection, opt, fillIn) {
if (!collection.IsVirtual()) {
// If we're not doing a "fetch add", delete all the current models
if (!opt['add'] && !opt.useset) {
// Reset with internal model
collection.reset(null, {'silent':true});
}
}
else {
// If we're not infilling based on an at, get, etc., delete all the current local models
if (!fillIn) {
collection._resetModelsToFullLength();
}
}
}
var opt = options || {}, success = opt['success'], self, errFunc = opt['error'];
if (opt['set']) {
opt.useset = opt['set'] ? true : false;
}
// Set up the parsing option
if (opt['parse'] === undefined) {
opt['parse'] = true;
}
self = this;
opt['error'] = function (xhr, status, error) {
oj.Model._triggerError(self, false, options, status, error, xhr);
if (errFunc) {
errFunc.call(oj.Model.GetContext(options, self), xhr, status, error);
}
};
opt['success'] = function (response, status, xhr) {
// Run the entire returned dataset through the collection's parser (either default no-op or user specified)
var data;
try {
data = self['parse'](response, options);
}
catch (e) {
oj.Collection._reportError(self, e, opt['error'], options);
return;
}
// Pull any virtualization properties out of the response
self._setPagingReturnValues(response, options, data, fillIn);
var dataList = null;
if (!opt['add'] && !self['model']) {
// We're not doing a "fetch add" and we don't have a model set on the collection
if (!fillIn) {
if (self.IsVirtual()) {
// Virtual case only
// Clean out the collection
doReset(self, opt, fillIn);
var manageLRU = false;
// Check for passed in offset
if (placement === -1) {
manageLRU = true;
placement = self._getOffset();
}
// Put the new data in
dataList = self._putDataIntoCollection(data, placement, manageLRU);
}
else {
// New backbone option to not reset
if (opt.useset) {
self._setInternal(data, false, opt, false);
}
else {
// Replace the data in the collection with a new set (non virtual)
self.reset(data, {'silent':true});
}
}
}
}
else {
// We have a model and/or we're doing a "fetch add"
// Clean out the old models if we're not "adding" or infilling for virtual
doReset(self, opt, fillIn);
// Parse each returned model (if appropriate), and put it into the collection, either from the zeroth offset if non-virtual
// or using the appropriate offset if virtual
try {
var manageLRU = false;
// Check for passed in offset
if (placement === -1) {
manageLRU = true;
placement = self._getOffset();
}
dataList = self._fillInCollectionWithParsedData(data, placement, manageLRU, opt);
}
catch (e) {
oj.Collection._reportError(self, e, opt['error'], options);
return;
}
}
if (self.IsVirtual()) {
// Take in the number of records actually fetched
if (dataList) {
self['lastFetchCount'] = dataList.length;
}
}
// Fire the sync event
var silent = opt['silent'] ? true : false;
self.TriggerInternal(silent, oj.Events.EventType['SYNC'], self, response, opt);
// Call the caller's success callback, if specified
if (success) {
success.call(oj.Model.GetContext(options, self), self, response, opt);
}
};
// Make the actual fetch call using ajax, etc.
return this._fetchCall(opt);
};
// Puts server data into an empty virtual collection using a "silent add"
oj.Collection.prototype._putDataIntoCollection = function(data, placement, manageLRU) {
var dataList;
if (data) {
dataList = (data instanceof Array) ? data : [data];
var addOpt = {};
// Only manage the LRU if we're not trying to achieve a range
if (manageLRU) {
this._manageLRU(dataList.length);
}
var insertPos = placement;
var virtual = this.IsVirtual();
var prevItem = null;
for (var i = 0; i < dataList.length; i=i+1) {
if (virtual) {
addOpt = {'at': insertPos};
prevItem = this._atInternal(insertPos, null, true, false);
}
// Don't fire add events
addOpt['silent'] = true;
this._addInternal(dataList[i], addOpt, true, false);
// If virtual, make sure the item was really added where we thought--in other words, what's there now shouldn't match what was there
// otherwise could be duplicate id and don't increment counter
if (this._atInternal(insertPos, null, true, false) !== prevItem) {
insertPos++;
}
}
}
return dataList;
};
// Parse each model returned, if appropriate, and push them into the (empty) collection with appropriate offsets if virtual
oj.Collection.prototype._fillInCollectionWithParsedData = function(data, placement, manageLRU, opt) {
opt = opt || {};
var parse = opt['parse'];
var modelInstance = oj.Collection._createModel(this);
if (data) {
var dataList = (data instanceof Array) ? data : [data];
var addOpt = {}, parsedModel;
// Only manage the LRU if we're not trying to achieve a range
if (manageLRU) {
this._manageLRU(dataList.length);
}
var virtual = this.IsVirtual();
if (opt.useset && !virtual) {
// New backbone option
for (var i = 0; i < dataList.length; i=i+1) {
if (modelInstance && parse) {
parsedModel = modelInstance.parse(dataList[i]);
}
else {
parsedModel = dataList[i];
}
dataList[i] = parsedModel;
}
this._setInternal(dataList, false, opt, false);
}
else {
var prevItem = null;
var insertPos = placement;
for (var i = 0; i < dataList.length; i=i+1) {
if (modelInstance && parse) {
parsedModel = modelInstance.parse(dataList[i]);
}
else {
parsedModel = dataList[i];
}
if (virtual) {
addOpt = {'at': insertPos};
prevItem = this._atInternal(insertPos, addOpt, true, false);
}
// Don't fire add events
addOpt['silent'] = true;
this._addInternal(parsedModel, addOpt, true, false);
// If virtual, make sure the item was really added where we thought--in other words, what's there now shouldn't match what was there
// otherwise could be duplicate id and don't increment counter
if (this._atInternal(insertPos, null, true, false) !== prevItem) {
insertPos++;
}
}
}
}
return dataList;
};
oj.Collection._reportError = function(collection, e, errorFunc, options) {
oj.Logger.error(e.toString());
if (errorFunc) {
errorFunc.call(oj.Model.GetContext(options, collection), collection, e, options);
}
};
// Used in virtualization to conduct server-based searches: returns list of fetched models via a promise but does not add them to the collection
// model list
oj.Collection.prototype._fetchOnly = function(options) {
var opt = options || {},
success = opt['success'],
parsedModel, self;
if (opt['parse'] === undefined) {
opt['parse'] = true;
}
self = this;
opt['success'] = function (response, status, xhr) {
var i, modelInstance, dataList = null, fetchedModels = [];
var data;
try {
data = self['parse'](response, options);
}
catch (e) {
oj.Collection._reportError(self, e, opt['error'], options);
return;
}
if (!opt['add'] && !self['model']) {
dataList = (data instanceof Array) ? data : [data];
}
else {
modelInstance = oj.Collection._createModel(self);
if (data) {
dataList = (data instanceof Array) ? data : [data];
for (i = 0; i < dataList.length; i=i+1) {
if (modelInstance && opt['parse']) {
try {
parsedModel = modelInstance.parse(dataList[i]);
}
catch (e) {
oj.Collection._reportError(self, e, opt['error'], options);
return;
}
}
else {
parsedModel = dataList[i];
}
fetchedModels.push(self._newModel(parsedModel));
}
}
}
self.TriggerInternal(false, oj.Events.EventType['SYNC'], self, response, opt);
if (success) {
success.call(oj.Model.GetContext(options, self), self, fetchedModels, opt);
}
};
return this._fetchCall(opt);
};
oj.Collection.prototype._fetchCall = function(opt) {
try {
return oj.Model._internalSync("read", this, opt);
}
catch (e) {
// Some kind of error: trigger an error event
oj.Model._triggerError(this, false, opt, null, e, null);
throw e;
}
};
oj.Collection.prototype._resetModelsToFullLength = function() {
var totalResults = this['totalResults'];
if (totalResults !== undefined && this._getModelsLength() !== totalResults) {
// Make sure to set up the array if the length changes (i.e., from 0 to totalResults--need to preallocate)
this._setModels(new Array(totalResults), true);
this._resetLRU();
this._setLength();
return true;
}
return false;
};
oj.Collection.prototype._getFetchSize = function(options) {
options = options || {};
return options[oj.Collection._FETCH_SIZE_PROP] || this[oj.Collection._FETCH_SIZE_PROP];
};
// Are we doing virtualization/paging?
oj.Collection.prototype.IsVirtual = function() {
return this._getFetchSize(null) > -1;
};
oj.Collection.prototype._getReturnProperty = function(customObj, response, property, optionValue, defaultValue) {
var value = parseInt(oj.Collection._getProp(customObj, response, property), 10);
if (value === undefined || value === null || isNaN(value)) {
// use fetchsize
return optionValue ? optionValue : defaultValue;
}
return value;
};
// Parse out some of the paging return values we might expect in a virtual response
oj.Collection.prototype._setPagingReturnValues = function(response, options, data, fillIn) {
var customObj = {};
// See if there's a custom call out
if (this['customPagingOptions']) {
customObj = this['customPagingOptions'].call(this, response);
if (!customObj) {
customObj = {};
}
}
// What limit was actually used to generate this response?
options = options || {};
this['lastFetchSize'] = this._getReturnProperty(customObj, response, 'limit', options['fetchSize'], this['fetchSize']);
// What offset was actually used to generate this response?
this['offset'] = this._getReturnProperty(customObj, response, 'offset', options['startIndex'], 0);
// How many records actually came back?
this['lastFetchCount'] = this._getReturnProperty(customObj, response, 'count', this['lastFetchCount'], this['lastFetchCount']);
// What is the total number of records possible for this collection?
this['totalResults'] = this._getReturnProperty(customObj, response, 'totalResults', this['totalResults'], this['totalResults']);
// Is there more?
this['hasMore'] = this._getHasMore(oj.Collection._getProp(customObj, response, 'hasMore'),
this['offset'], this['lastFetchSize'], this['totalResults']);
// Adjust total results to account for the case where the server tells us there's no more data, and totalResults wasn't set
// by the server...but don't do it for simple gets/adds
if (!fillIn) {
// We want to know if the server *actually* returned values for these things, not if they defaulted above
var totalResultsReturned = parseInt(oj.Collection._getProp(customObj, response, 'totalResults'),10);
var lastFetchCountReturned = parseInt(oj.Collection._getProp(customObj, response, 'count'), 10);
this['totalResults'] = this._adjustTotalResults(totalResultsReturned, this['hasMore'], this['offset'], lastFetchCountReturned, data && Array.isArray(data) ? data.length : 0);
}
// Was fetchSize set? If not, set it to limit
if (!this.IsVirtual() && this['totalResults'] && this['totalResults'] !== this['lastFetchCount'] && this['lastFetchSize']) {
this.setFetchSize(this['lastFetchSize']);
}
};
oj.Collection.prototype._adjustTotalResults = function(totalResultsReturned, hasMore, offset, lastFetchCount, dataLength) {
// Fix for bug 21239986: if hasMore is false, and totalResults wasn't set by the server, we should set it to the last
// thing fetched here. There is no more.
if (!hasMore) {
// No more data, per server
if (isNaN(totalResultsReturned)) {
// TotalResults was not returned by the server here
// If lastFetchCount was set, use that as the final total. Otherwise, use the length of data passed in...all + offset
var count = !isNaN(lastFetchCount) ? lastFetchCount : dataLength;
return count + offset;
}
}
return this['totalResults'];
};
oj.Collection.prototype._getHasMore = function(hasMore, offset, lastFetchSize, totalResults) {
if (oj.Collection._defined(hasMore)) {
return hasMore;
}
// Special case: return true if totalResults not defined
if (totalResults === undefined || totalResults === null) {
return true;
}
// Not there: figure it out. It's true unless we're walking off the end
return (offset + lastFetchSize > totalResults) ? false : true;
};
oj.Collection._getProp = function(custom, response, prop) {
if (custom.hasOwnProperty(prop)) {
return custom[prop];
}
return response ? response[prop] : undefined;
};
oj.Collection.prototype._getOffset = function() {
return (oj.Collection._defined(this['offset']) ? this['offset'] : 0);
};
/**
* Creates a new model, saves it to the data service, and adds it on to the collection.<p>
* Events:<p>
* <ul>
* <b>add</b>: fired when the model is added, passing the collection, model added, and options, if not silent<br>
* <b>alladded</b>: fired after all models have been added, passing the collection, array of models added, and options<br>
* <b>request:</b>: fired when the request to save is going to the server<br>
* <b>sync:</b>: fired when the model is saved to the data service, passing the model and the raw response<br>
* </ul>
* @param {Object=} attributes Set of attribute/value pairs with which to initialize the new model object, or a new Model object
* @param {Object=} options Options to control save (see [save]{@link oj.Model#save}). Plus:<p>
* <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not<br>
* @return {Object|boolean|Promise} new model or false if validation failed. If virtual, returns a promise that resolves with the new model
* @export
*/
oj.Collection.prototype.create = function (attributes, options) {
var self = this;
options = options || {};
var deferred = this._getDeferred(options);
function doSave(collection, newModel, validate, opt) {
newModel.save(attributes instanceof oj.Model ? null : attributes, opt);
return newModel;
}
function doAdd(newModel, addOpts) {
if (options['wait']) {
// Don't do adding now
if (self.IsVirtual() || deferred) {
return self._addPromise(function() {
return Promise.resolve(undefined);
});
}
return;
}
return self.add(newModel, addOpts);
}
// Save the user's context and callback, if any
var newModel = this._newModel(attributes, true, options, false),
callback = options['success'],
context = options['context'], validate = options['validate'];
options['context'] = this;
options['success'] = function(model, resp, successOpts) {
// Make sure we pass xhr
if (successOpts['xhr']) {
options['xhr'] = successOpts['xhr'];
}
if (options['wait']) {
// Trigger delayed add events
self.add(newModel, options);
}
if (callback) {
callback.call(context != null ? context : self, model, resp, options);
}
};
// Did validation pass?
if (newModel == null) {
return false;
}
// Force a save in case user has set value of idAttribute on the new Model
options['forceNew'] = newModel.GetId() != null;
var addOpts = oj.Model._copyOptions(options);
newModel.SetCollection(this);
if (deferred || this.IsVirtual()) {
return new Promise(function (resolve, reject) {
addOpts['merge'] = true;
addOpts['deferred'] = true;
//if (options['wait']) {
// addOpts['silent'] = true;
// }
//self.add(newModel, addOpts)
doAdd(newModel, addOpts)
.then(function() {
options['success'] = function(model, resp, successOpts) {
// Make sure we pass xhr
if (successOpts['xhr']) {
options['xhr'] = successOpts['xhr'];
}
if (options['wait']) {
// Trigger delayed add event
// Force the add if virtual because we know it was successfully saved: we don't
// want to go back to the server to check
if (self.IsVirtual()) {
addOpts['force'] = true;
}
self.add(newModel, addOpts).then(function() {
if (callback) {
callback.call(context != null ? context : self, model, resp, options);
}
resolve(model);
});
}
else {
if (callback) {
callback.call(context != null ? context : self, model, resp, options);
}
resolve(model);
}
};
var model = doSave(self, newModel, validate, options);
// make sure that success is called first if successful...promise resolved
// second
if (!model) {
// Failed: make sure we resolve the promise. Otherwise promise will
// be resolved by success call, above
resolve(model);
}
});
});
}
addOpts['merge'] = true;
//if (options['wait']) {
// addOpts['silent'] = true;
// }
//this.add(newModel, addOpts);
doAdd(newModel, addOpts);
var model = doSave(this, newModel, validate, options);
return model;
};
/**
* Return a list of all the values of attr found in the collection
*
* @param {string} attr attribute to return
* @return {Array} array of values of attr
*
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.pluck = function(attr) {
var arr = [], i;
this._throwErrIfVirtual("pluck");
for (i = 0; i < this._getLength(); i=i+1) {
arr.push(this.at(i).get(attr));
}
return arr;
};
/**
* Return an array of models that contain the given attribute/value pairs. Note that this function, along with findWhere, expects server-resolved
* filtering to return *all* models that meet the criteria, even in virtual cases. The fetchSize will be set to the value of totalResults for this call to indicate that
* all should be returned.<br>
* Events: for events, if virtual, see [fetch]{@link oj.Collection#fetch}
*
* @param {Object|Array} attrs attribute/value pairs to find. The attribute/value pairs are ANDed together. If attrs is an array of attribute/value pairs, then these are ORed together
* If the value is an object (or an array of objects, in which case the single attribute must meet all of the value/comparator conditions), then if it has both 'value' and 'comparator' parameters these will be interpreted as
* expressions needing custom commparisons. The comparator value may either be a string or a comparator callback function.
* Strings are only valid where the filtering is sent back to the data service (virtual collections). In the case of a comparator
* function, the function always takes the signature function(model, attr, value), and for non-virtual collections, is called for each
* Model in the collection with the associated attribute and value. The function should return true if the model meets the attribute/value
* condition, and false if not. For cases where the filtering is to be done on the server, the function will be called once per attr/value pair
* with a null model, and the function should return the string to pass as the comparison in the expression for the filtering parameter
* in the URL sent back to the server. Note that the array of value object case is really only meaningful for server-evaluated filters where
* a complex construction on a single attribute might be needed (e.g., x>v1 && x <=v2)
* For example:<p>
* {Dept:53,Name:'Smith'}<br>
* will return an array of models that have a Dept=53 and a Name=Smith, or, for server-filtered
* collections, a ?q=Dept=53+Name=Smith parameter will be sent with the URL.<p>
* [{Dept:53},{Dept:90}]<br>
* will return all models that have a Dept of 53 or 90. Or, ?q=Dept=53,Dept=90 will be sent to the server.<p>
* {Dept:{value:53,comparator:function(model, attr, value) { return model.get(attr) !== value;}}}<br>
* will return all models that do not have a Dept of 53.<p>
* {Dept:{value:53,comparator:'<>'}}<br>
* For server-evaluated filters, a parameter ?q=Dept<>53 will be sent with the URL. This form is an
* error on locally-evaluated colleection filters<p>
* {Dept:{value:53,comparator:function(model, attr, value) { return "<>";}}}<br>
* expresses the same thing for server-evaluated filters<p>
* {Dept:[{value:53,comparator:'<'},{value:90,comparator:'<'}]}<br>
* For server-evaluated filters, a parameter ?q=Dept>53+Dept<93 will be sent to the server<p>
* @param {Object=} options <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not<p>
*
* @return {Array|Promise} array of models. If virtual or deferred, a promise that resolves with the returned array from the server
* @export
*/
oj.Collection.prototype.where = function(attrs, options) {
return this._handlePromise(this._whereInternal(attrs, options));
};
oj.Collection.prototype._whereInternal = function(attrs, options) {
options = options || {};
var deferred = this._getDeferred(options);
var self = this;
if (this.IsVirtual()) {
return new Promise(function(resolve, reject) {
var success = function(collection, fetchedModels, options) {
resolve(fetchedModels);
};
// Send the attributes for a server-based filter; also indicate that we need *all* the attributes. In the standard
// REST URL construction this is accomplished by leaving off fetchSize/start indices, etc.
var opt = {'query':attrs,
'all': true,
'success': success,
'error': function(xhr, status, error) {
reject(oj.Collection._createRejectionError(xhr, status, error, self, options));
}};
self._fetchOnly(opt);
});
}
var arr = [], i, m;
for (i = 0; i < this._getLength(); i=i+1) {
m = this.at(i);
if (m.Contains(attrs)) {
arr.push(m);
}
}
if (deferred) {
return new Promise(function(resolve, reject) {
resolve(arr);
});
}
return arr;
};
/**
* Return a collection of models that contain the given attribute/value pairs.
* Note that this returns a non-virtual collection with all the models returned by the server,
* even if the original collection is virtual. Virtual collections doing filtering on the server should return all models that meet
* the critera. See [where]{@link oj.Collection#where}
* See [where]{@link oj.Collection#where} for complete documentation of events and parameters
*
* @return {Object|Promise} A collection containing models with given attribute/value pairs. If virtual or deferred, a promise that resolves with the collection returned by the server
* @export
*/
oj.Collection.prototype.whereToCollection = function(attrs, options) {
options = options || {};
var deferred = this._getDeferred(options);
var self = this;
if (this.IsVirtual() || deferred) {
return this._addPromise(function() {
return new Promise(function(resolve, reject) {
return self._whereInternal(attrs, options).then(function (models) {
var collection = self._makeNewCollection(models);
resolve(collection);
}, function(err) {
reject(err);
});
});
});
}
else {
var models = this._whereInternal(attrs, options);
var newCollection = this._makeNewCollection(models);
newCollection[oj.Collection._FETCH_SIZE_PROP] = -1;
newCollection._setLength();
return newCollection;
}
};
oj.Collection.prototype._makeNewCollection = function(models) {
var collection = this._cloneInternal(false);
collection._setModels(models, false);
collection._resetLRU();
collection._setLength();
return collection;
};
oj.Collection.prototype._throwErrIfVirtual = function(func) {
if (this.IsVirtual()) {
throw new Error(func + " not valid on a virtual Collection");
}
};
/**
* Return an array whose entries are determined by the results of calling the passed iterator function. The iterator will be called for each model in the collection
*
* @param {function(Object)} iterator function to determine the mapped value for each model
* @param {Object=} context context with which to make the calls on iterator
* @returns {Array} array of values determined by the return value of calls made to iterator for each model
*
* @throws {Error} when called on a virtual Collection
* @export
*/
oj.Collection.prototype.map = function(iterator, context) {
var retArr = [], value;
this._throwErrIfVirtual("map");
this._getModels().forEach(function(model) {
value = iterator.call(context || this, model);
retArr.push(value);
});
return retArr;
};
/**
* @desc Iterates over the models in the collection and calls the given iterator function
*
* @param {function(Object)} iterator function to call for each model
* @param {Object=} context context with which to make the calls on iterator
*
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.each = function(iterator, context) {
this._throwErrIfVirtual("each");
this._getModels().forEach(iterator, context);
};
/**
* Return the length of the collection
* @returns {number} length of the collection
* @export
*/
oj.Collection.prototype.size = function() {
return this._getLength();
};
/**
* Return the models sorted determined by the iterator function (or property, if a string value). If a function, the function should return the attribute by which to sort.
*
* @param {String|function(Object)} iterator method called or property used to get the attribute to sort by.
* @param {Object=} context context with which to make the calls on iterator
* @returns {Object} models sorted using iterator
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.sortBy = function(iterator, context) {
var retArr = [], self;
this._throwErrIfVirtual("sortBy");
this._getModels().forEach(function(model) {
retArr.push(model);
});
self = this;
retArr.sort(function(a, b) {
var keyA, keyB;
if ($.isFunction(iterator)) {
// "sortBy" comparator option
keyA = iterator.call(context || self, a);
keyB = iterator.call(context || self, b);
return oj.Collection._compareKeys(keyA, keyB, self['sortDirection']);
}
// String option
keyA = a.get(iterator);
keyB = b.get(iterator);
return oj.Collection._compareKeys(keyA, keyB, self['sortDirection']);
});
return retArr;
};
/**
* @desc Return the collection with models grouped into sets determined by the iterator function (or property, if a string value)
*
* @param {String|function(Object)} iterator method called or property (if a string) used to get the group key
* @param {Object=} context context with which to make the calls on iterator
* @returns {Object} models grouped into sets
*
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.groupBy = function(iterator, context) {
var retObj = {}, groupVal;
this._throwErrIfVirtual("groupBy");
this._getModels().forEach(function(model) {
if ($.isFunction(iterator)) {
groupVal = iterator.call(context || this, model);
}
else {
groupVal = model.get(iterator);
}
if (retObj[groupVal] === undefined) {
retObj[groupVal] = [];
}
retObj[groupVal].push(model);
}, this);
return retObj;
};
/**
* @desc Return an object with models as values for their properties determined by the iterator function or property string
*
* @param {String|function(Object)} iterator method called or property (if a string) used to get the index attribute
* @param {Object=} context context with which to make the calls on iterator
* @returns {Object} models listed as property values where the properties are the values returned by iterator or the attribute value given by the iterator string
*
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.indexBy = function(iterator, context) {
var retObj = {}, index;
this._throwErrIfVirtual("indexBy");
this._getModels().forEach(function(model) {
if ($.isFunction(iterator)) {
index = iterator.call(context || this, model);
}
else {
index = model.get(iterator);
}
retObj[index] = model;
}, this);
return retObj;
};
/**
* Return the "minimum" model in the collection, as determined by calls to iterator. The return value of iterator (called with a model passed in) will be compared against the current minimum
*
* @param {function(Object)} iterator function to determine a model's value for checking for the minimum
* @param {Object=} context context with which to make the calls on iterator
* @returns {Object} "Minimum" model in the collection
*
* @throws {Error} when called on a virtual Collection
* @export
*/
oj.Collection.prototype.min = function(iterator, context) {
var minModel = {}, minModelValue, currValue;
this._throwErrIfVirtual("min");
if (this._getModelsLength() == 0) {
return null;
}
// Get vals started
minModel = this._getModel(0);
minModelValue = iterator.call(context || this, this._getModel(0));
this._getModels().forEach(function(model, i) {
if (i >= 1) {
currValue = iterator.call(context || this, model);
if (currValue < minModelValue) {
minModel = model;
minModelValue = currValue;
}
}
}, this);
return minModel;
};
/**
* Return the "maximum" model in the collection, as determined by calls to iterator. The return value of iterator (called with a model passed in) will be compared against the current maximum
*
* @param {function(Object)} iterator function to determine a model's value for checking for the maximum
* @param {Object=} context context with which to make the calls on iterator
* @returns {Object} "Maximum" model in the collection
*
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.max = function(iterator, context) {
var maxModel = {}, maxModelValue, currValue;
this._throwErrIfVirtual("max");
if (this._getModelsLength() == 0) {
return null;
}
// Get vals started
maxModel = this._getModel(0);
maxModelValue = iterator.call(context, this._getModel(0));
this._getModels().forEach(function(model, i) {
if (i >= 1) {
currValue = iterator.call(context || this, model);
if (currValue > maxModelValue) {
maxModel = model;
maxModelValue = currValue;
}
}
}, this);
return maxModel;
};
/**
* @export
* @desc Return an array of models that cause passed-in iterator to return true
*
* @param {function(Object)} iterator function to determine if a model should be included or not. Should return true or false
* @param {Object=} context context with which to make the calls on iterator
* @returns {Array} array of models that caused iterator to return true
*
* @throws {Error} when called on a virtual Collection
*/
oj.Collection.prototype.filter = function(iterator, context) {
var retArr = [];
this._throwErrIfVirtual("filter");
this._getModels().forEach(function(model) {
if (iterator.call(context || this, model)) {
retArr.push(model);
}
});
return retArr;
};
/**
* Return an array of models minus those passed in as arguments
* @param {...Object} var_args models models to remove from the returned array
* @returns {Array} array of models from the collection minus those passed in to models
*
* @throws {Error} when called on a virtual Collection
* @export
*/
oj.Collection.prototype.without = function(var_args) {
var retArr = [], j, id, cid, add;
this._throwErrIfVirtual("without");
var model;
// Test each model in the collection
for (var i = 0; i < this._getModels().length; i++) {
add = true;
model = this._getModel(i);
for (j = 0; j < arguments.length; j=j+1) {
// Get the cid
cid = arguments[j].GetCid();
// Get the id
id = arguments[j].GetId();
if (model.Match(id, cid)) {
// If it's found, don't return it--we're "subtracting" those from the return value, which starts as all models in the collection
add = false;
break;
}
}
if (add) {
retArr.push(model);
}
}
return retArr;
};
/**
* Return an array of models in the collection but not passed in the array arguments
* @param {...Array} var_args models arrays of models to check against the collection
* @returns {Array} array of models from the collection not passed in as arguments
*
* @throws {Error} when called on a virtual Collection
* @export
*/
oj.Collection.prototype.difference = function(var_args) {
var retArr = [], j, k, id, cid, add;
this._throwErrIfVirtual("difference");
var model;
for (var i = 0; i < this._getModels().length; i++) {
add = true;
model = this._getModel(i);
for (j = 0; j < arguments.length; j=j+1) {
// Each argument is assumed to be an array of oj.Models
for (k = 0; k < arguments[j].length; k++) {
// Get the cid
cid = arguments[j][k].GetCid();
// Get the id
id = arguments[j][k].GetId();
if (model.Match(id, cid)) {
add = false;
break;
}
}
if (!add) {
// We found this model somewhere in the arguments--we're not going to add it so get out
break;
}
}
if (add) {
retArr.push(model);
}
}
return retArr;
};
/**
* Determine if the collection has any models
*
* @returns {boolean} true if collection is empty
* @export
*/
oj.Collection.prototype.isEmpty = function() {
return this._getLength() === 0;
};
/**
* @export
* @desc Return true if any of the models in the collection pass the test made by calling the iterator function parameter
*
* @param {function(Object)} iterator function called with each model to determine if it "passes". The function should return true or false.
* @param {Object=} context context with which to make the calls on iterator
* @returns {boolean} true if any of the models cause the iterator function to return true
*
* @throws {Error} when called on a virtual collection
*/
oj.Collection.prototype.any = function(iterator, context) {
this._throwErrIfVirtual("any");
var model;
for (var i = 0; i < this._getModelsLength(); i=i+1) {
model = this._getModel(i);
if (iterator.call(context || this, model)) {
return true;
}
}
return false;
};
/**
* @export
* @desc A version of where that only returns the first element found<br>
* Events: for events, if virtual, see [fetch]{@link oj.Collection#fetch}<br>
* @param {Object|Array} attrs attribute/value pairs to find.
* See [where]{@link oj.Collection#where} for more details and examples.
* @param {Object=} options <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not<p>
*
* @returns {Object|Promise} first model found with the attribute/value pairs. If virtual or deferred, a promise that resolves with the returned array from the server
*/
oj.Collection.prototype.findWhere = function(attrs, options) {
var deferred = this._getDeferred(options);
var self = this;
if (this.IsVirtual() || deferred) {
return this._addPromise(function() {
return new Promise(function(resolve, reject) {
self._whereInternal(attrs, options).then(function(modelList) {
if (modelList && modelList.length > 0) {
resolve(modelList[0]);
}
resolve(null);
});
});
});
}
var arr = this._whereInternal(attrs, options);
if (arr.length > 0) {
return arr[0];
}
return null;
};
/**
* Return a shallow copy of the models from start to end (if specified), in an array<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
*
* @param {number} start model to start the return array with
* @param {number=} end model to end the return array with, if specified (not inclusive). If not, returns to the end of the collection
* @param {Object=} options <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
* @return {Array|Promise} array of model objects from start to end, or a promise that resolves specifying the returned array when done
* @export
*/
oj.Collection.prototype.slice = function(start, end, options) {
var deferred = this._getDeferred(options);
var ret = [], i;
if (end === undefined) {
if (this.IsVirtual() && !this._hasTotalResults()) {
// We can't set the end: throw an error
throw new Error("End must be set for virtual collections with no totalResults");
}
end = this._getModelsLength();
}
if (deferred || this.IsVirtual()) {
return this._addPromise(function() {
// Loop using deferred
return this.IterativeAt(start, end);
});
}
for (i = start; i < end; i=i+1) {
ret.push(this._getModel(i));
}
return ret;
};
/**
* Update the collection with a model or models. Depending on the options, new models will be added, existing models will be merged, and unspecified models will be removed.
* The model cid is used to determine whether a given model exists or not.<br>
* May fire events as specified by [add]{@link oj.Collection#add} or [remove]{@link oj.Collection#remove}, depending on the options.
*
* @param {Object} models an array of or single model with which to update the collection
* @param {Object=} options <b>add</b>:false stops the addition of new models<br>
* <b>remove</b>: false stops the removal of missing models<br>
* <b>merge</b>: false prevents the merging of existing models<br>
* <b>silent</b>: true prevents notifications on adds, removes, etc.<br>
* <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
*
* @returns {Promise|null} if deferred or virtual, return a promise that resolves when the set has completed
* @export
*/
oj.Collection.prototype.set = function(models, options) {
var deferred = this._getDeferred(options);
return this._setInternal(models, true, options, deferred || this.IsVirtual());
};
oj.Collection._removeAfterSet = function(collection, models, remove, foundModels, options) {
// Now remove models that weren't found
// get an array of all models
// Can't avoid looping over everything because we *have* to clean up even unfetched models, in order to fire events, etc.
if (remove) {
for (var i = models.length-1; i >= 0; i=i-1) {
if (foundModels.indexOf(i) == -1) {
collection._removeInternal(models[i], i, options);
}
}
}
};
// Swap two models, and indicate if anything was actually swapped
oj.Collection.prototype._swapModels = function(oldIndex, newIndex, remove, add) {
if (this._hasComparator() || !remove || !add) {
return {index:oldIndex, swapped:false};
}
// Make sure in range
var len = this._getModelsLength();
if (oldIndex >= len || newIndex >= len) {
return {index:oldIndex, swapped:false};
}
// Swap
var oldModel = this._getModel(oldIndex);
var newModel = this._getModel(newIndex);
//this._getModels()[oldIndex] = newModel;
this._setModel(oldIndex, newModel);
newModel.SetIndex(oldIndex);
//this._getModels()[newIndex] = oldModel;
this._setModel(newIndex, oldModel);
oldModel.SetIndex(newIndex);
return {index:newIndex, swapped:(newIndex!==oldIndex)};
};
oj.Collection.prototype._setInternal = function(models, parse, options, deferred) {
// Determine if any of the options are set
options = options || {};
var add = options['add'] === undefined ? true : options['add'],
remove = options['remove'] === undefined ? true : options['remove'],
merge = options['merge'] === undefined ? true : options['merge'],
foundModels = [], currModel = null, i, modelList;
if (parse) {
models = this['parse'](models);
}
modelList = $.isArray(models) ? models : [models];
if (deferred) {
return this._addPromise(function() {
return this._deferredSet(modelList, this._getModels(), options, remove, add, merge, parse);
});
}
// Go through the passed in models and determine what to do
var swapped = false;
for (i = 0; i < modelList.length; i=i+1) {
currModel = this._updateModel(this._newModel(modelList[i], parse, options, true), add, merge, deferred);
if (currModel !== -1) {
// And swap it into the position as passed in, if no comparator and we're removing, so that the slots are exact
var obj = this._swapModels(currModel, i, remove, add);
var newLoc = obj.index;
if (obj.swapped) {
swapped = true;
}
// Save its new location as found
if (foundModels.indexOf(newLoc) === -1) {
foundModels.push(newLoc);
}
}
}
if (swapped) {
var eventOpts = options['add'] ? {'add':true} : null;
this.TriggerInternal(options['silent'], oj.Events.EventType['SORT'], this, eventOpts, null);
}
oj.Collection._removeAfterSet(this, this._getModels(), remove, foundModels, options);
};
// Handle the updates/removes on virtual collections
oj.Collection.prototype._deferredSet = function(modelList, modelsCopy, options, remove, add, merge, parse) {
var foundModels = [], i;
// Go through the passed in models and determine what to do
var self = this;
return new Promise(function(allResolve, allReject) {
var doTask = function(index) {
return new Promise(function(resolve, reject) {
self._updateModel(self._newModel(modelList[index], parse, options, true), add, merge, true).then(function (currModel) {
if (currModel !== -1) {
foundModels.push(currModel);
}
resolve(index+1);
}, reject);
});
};
var currentStep = Promise.resolve(0);
for (i = 0; i < modelList.length; i=i+1) {
currentStep = currentStep.then(doTask);
}
currentStep.then(function() {
oj.Collection._removeAfterSet(self, modelsCopy, remove, foundModels, options);
allResolve(undefined);
}, allReject);
});
};
// Return the index of the given model after updating it, if it was found. Otherwise it is added and a -1 is returned
oj.Collection.prototype._updateModel = function(model, add, merge, deferred) {
function update(collection, found, deferred) {
var index = found ? found.index : -1;
var foundModel = found ? found['m'] : null;
if (foundModel) {
if (merge) {
// Do merge if not overridden
var opt = {'merge':merge};
if (deferred) {
return new Promise(function(resolve, reject) {
collection._addInternal(model, opt, false, true).then(function() {
resolve(index);
});
});
}
collection.add(model, opt);
}
}
else if (add) {
if (deferred) {
return new Promise(function(resolve, reject) {
collection._addInternal(model, opt, false, true).then(function() {
resolve(collection._getLength()-1);
});
});
}
collection.add(model);
index = collection._getLength()-1;
}
return index;
}
// Check to see if this model is in the collection
if (deferred || this.IsVirtual()) {
var self = this;
return new Promise(function(resolve, reject) {
self._getInternal(model, {'silent':true}, deferred).then(function(found) {
update(self, found, true).then(function (index) {
resolve(index);
});
});
});
}
var found = this._getInternal(model);
return update(this, found, false);
};
/**
* Return a copy of the Collection's list of current attribute/value pairs.
*
* @return {Array|Promise} an array containing all the Collection's current sets of attribute/value pairs. If virtual, a promise that will resolve with that array
*
* @export
*/
oj.Collection.prototype.toJSON = function() {
var retArr = [];
this._throwErrIfVirtual("toJSON");
/* if (this.IsVirtual()) {
return this._addPromise(function() {
return this.IterativeAt(0, this._getLength());
});
}*/
this._getModels().forEach(function(model) {
retArr.push(model.toJSON());
});
return retArr;
};
/**
* Return the first model object in the collection, or an array of the first n model objects from the collection.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
* @param {number=} n Number of model objects to include in the array, starting with the first.
* @param {Object=} options deferred: if true, return a promise as though this collection were virtual whether it is or not
* @return {Array|null|Promise} An array of n model objects found in the collection, starting with the first. If n is not included,
* returns all of the collection's models as an array. If deferred or virtual, returns a promise that resolves with the array or model
* @export
*/
oj.Collection.prototype.first = function(n, options) {
var deferred = this._getDeferred(options);
var elementCount = this._getLength(),
retArray = [],
i;
if (n) {
elementCount = n;
}
else {
n = 1;
}
var virtual = this.IsVirtual() || deferred;
if (n === 1) {
if (virtual) {
return this._deferredAt(0, null);
}
if (this._getModelsLength() > 0) {
return this._getModel(0);
}
return null;
}
if (elementCount > this._getModelsLength()) {
if (this.IsVirtual() && !this._hasTotalResults()) {
// Virtual, no total results: don't restrict elementCount
}
else {
elementCount = this._getModelsLength();
}
}
if (virtual) {
return this._addPromise(function() {
return this.IterativeAt(0, elementCount);
});
}
for (i = 0; i < elementCount; i=i+1) {
retArray.push(this._getModel(i));
}
return retArray;
};
/**
* Return the array index location of the given model object.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
* @param {Object} model Model object (or Model id) to locate
* @param {Object=} options <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
* @return {number|Promise} The index of the given model object, or a promise that will resolve with the index when complete.
* If the object is not found, returns -1.
* @export
*/
oj.Collection.prototype.indexOf = function(model, options) {
var location;
var deferred = this._getDeferred(options);
if (this.IsVirtual() || deferred) {
return this._getInternal(model, null, true).then(function(loc) {
return loc.index;
});
}
location = this._getInternal(model);
return location.index;
};
/**
* Determine if the given model object is present in the collection.<br>
* For events that may be fired if the collection is virtual, see [fetch]{@link oj.Collection#fetch}.<br>
* @param {Object} model Model object (or Model id) to locate
* @param {Object=} options <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not
* @return {boolean|Promise} true if the model is contained in the collection, false if not. If deferred, a promise that will resolve with true or false when complete.
* @export
*/
oj.Collection.prototype.contains = function(model, options) {
var location;
var deferred = this._getDeferred(options);
if (this.IsVirtual() || deferred) {
return this._getInternal(model, null, true).then(function(loc) {
return loc.index > -1;
});
}
location = this._getInternal(model);
return location.index > -1;
};
/**
* An alias for [contains]{@link oj.Collection#contains}
* @export
*/
oj.Collection.prototype.include = oj.Collection.prototype.contains;
// Only look on models already fetched
oj.Collection.prototype._localIndexOf = function(model) {
var location = this._getLocalInternal(model);
return location !== undefined ? location.index : -1;
};
/**
* Remove the last model from the collection and return it<br>
* For events that may be fired if the collection is virtual, see [remove]{@link oj.Collection#remove}.<br>
* @param {Object=} options Options for the method:<p>
* <b>silent</b>: if set, do not fire a remove event <br>
* <b>deferred</b>: if true, return a promise as though this collection were virtual whether it is or not<br>
* @return {Object|Promise} the model that was removed, or a promise that will resolve with the model that was removed when complete
* @export
*/
oj.Collection.prototype.pop = function(options) {
var deferred = this._getDeferred(options);
if (this.IsVirtual() || deferred) {
var self = this;
return this.at(this._getLength()-1, {'deferred':deferred}).then(function (model) {
self.remove(model, options);
return model;
});
}
var m = this.at(this._getLength()-1);
this.remove(m, options);
return m;
};
/**
* Add the given model to the end of the Collection<br>
* For events that may be fired if the collection is virtual, see [add]{@link oj.Collection#add}.<br>
* @param {Object} m model to add to the end of the Collection
* @param {Object=} options same options as [add]{@link oj.Collection#add}
* @return {Promise} if deferred or virtual, a promise that will be resolved when the function is done. Otherwise undefined
* @export
*/
oj.Collection.prototype.push = function(m, options) {
var deferred = this._getDeferred(options);
this._manageLRU(1);
return this._handlePromise(this._addInternal(m, options, false, deferred));
};
/**
* Returns the index of the last location of the given model. Not supported in virtual cases.
* @param {Object} model Model object to locate
* @param {number=} fromIndex optionally start search at the given index
* @return {number} The last index of the given model object. If the object is not found, returns -1.
* @throws {Error} when called on a virtual collection
* @export
*/
oj.Collection.prototype.lastIndexOf = function(model, fromIndex) {
var i;
this._throwErrIfVirtual("lastIndexOf");
if (fromIndex === undefined) {
fromIndex = 0;
}
for (i = this._getLength()-1; i >= fromIndex; i=i-1) {
if (oj.Object.__innerEquals(model, this.at(i))) {
return i;
}
}
return -1;
};
oj.Collection.prototype._getSortAttrs = function(sortStr) {
if (sortStr === undefined) {
return [];
}
return sortStr.split(",");
};
// Return a URL query string based on an array of or a single attr/value pair set
oj.Collection._getQueryString = function(q) {
function expression(left, right, compare) {
return left + compare + right;
}
var queries = $.isArray(q) ? q : [q];
var str = "", query, exp, i, prop;
for (i = 0; i < queries.length; i++) {
query = queries[i];
for (prop in query) {
if (query.hasOwnProperty(prop)) {
var val = $.isArray(query[prop]) ? query[prop] : [query[prop]];
for (var j = 0; j < val.length; j++) {
if (oj.Model.IsComplexValue(val[j])) {
var value = val[j]['value'];
var compare = null;
var comparator = val[j]['comparator'];
if ($.isFunction(comparator)) {
compare = comparator(null, prop, value);
}
else {
compare = comparator;
}
exp = expression(prop, value, compare);
}
else {
exp = expression(prop, query[prop], "=");
}
str += exp + "+";
}
}
}
// Remove trailing '+'
str = str.substring(0, str.length-1) + ",";
}
// Remove trailing ','
if (str.substring(str.length-1) === ",") {
return str.substring(0, str.length-1);
}
return str;
};
oj.Collection.prototype.ModifyOptionsForCustomURL = function(options) {
var opt = {};
for (var prop in options) {
if (options.hasOwnProperty(prop)) {
opt[prop] = options[prop];
}
}
var comparator = this['comparator'];
if (comparator && oj.StringUtils.isString(comparator)) {
var attrs = this._getSortAttrs(comparator);
for (var i = 0; i < attrs.length; i++) {
if (i === 0) {
opt['sort'] = attrs[i];
}
else {
opt['sort'] += "," + attrs[i];
}
}
opt['sortDir'] = this._getSortDirStr();
}
// Put fetchSize on if appropriate, and not already set
if (this.IsVirtual()) {
opt[oj.Collection._FETCH_SIZE_PROP] = this._getFetchSize(opt);
}
return opt;
};
// Determine if this collection is URL-based
oj.Collection.prototype.IsUrlBased = function(options) {
var customURL = this['customURL'];
if ($.isFunction(customURL)) {
return true;
}
var url = this.GetCollectionFetchUrl(options);
return oj.Collection._defined(url);
};
// Build a URL with parameters for the collection fetch
oj.Collection.prototype.GetCollectionFetchUrl = function(options) {
var url = oj.Model.GetPropValue(this, 'url');
// Adorn it with options, if any
if (this.IsVirtual()) {
var all = options['all'];
// Put in page size
var limit = null;
if (all) {
var totalResults = this['totalResults'];
limit = totalResults ? totalResults : this._getFetchSize(options);
}
else {
limit = this._getFetchSize(options);
}
if (url && url.indexOf("?") > -1) {
// Already have a param coming in
url += "&";
}
else {
url += "?";
};
url += "limit=" + limit;
if (!all) {
if (oj.Collection._defined(options['startIndex'])) {
url += "&offset=" + options['startIndex'];
}
if (options['startID']) {
url += "&fromID=" + options['startID'];
}
if (options['since']) {
url += "&since=" + options['since'];
}
if (options['until']) {
url += "&until=" + options['until'];
}
}
// Query
if (options['query']) {
var queryString = oj.Collection._getQueryString(options['query']);
if (queryString && queryString.length > 0) {
url += "&q=" + queryString;
}
}
// Add sorting
var comparator = this['comparator'];
if (comparator && oj.StringUtils.isString(comparator)) {
var attrs = this._getSortAttrs(comparator);
var sortDirStr = this._getSortDirStr(), i;
for (i = 0; i < attrs.length; i++) {
if (i === 0) {
url += "&orderBy=" + attrs[i] + ":" + sortDirStr;
}
else {
url += "," + attrs[i] + ":" + sortDirStr;
}
}
}
// Always ask for totalresults
url +="&totalResults=true";
}
return url;
};
oj.Collection.prototype._getSortDirStr = function() {
if (this['sortDirection'] === -1) {
return "desc";
}
return "asc";
};
/**
* Called to perfrom server interactions, such as reading the collection. Designed to be overridden by users
*
* @param {string} method "read"
* @param {Object} collection the collection to be read (fetched)
* @param {Object=} options to control sync<br>
* <b>success</b>: called if sync succeeds. Called with the array of JSON data being fetched<br>
* <b>error</b>: called if sync fails. Called with xhr, status, and error info, per jQuery Ajax (all if available)<br>
* @return {Object} xhr response from ajax by default
* @alias oj.Collection.prototype.sync
*/
oj.Collection.prototype['sync'] = function(method, collection, options) {
return window['oj']['sync'](method, collection, options);
};
// Constants
oj.Collection._FETCH_SIZE_PROP = 'fetchSize';
Source: src/main/javascript/oracle/oj/ojmodel/Collection.js
Oracle® JavaScript Extension Toolkit (JET)
1.1.2
E65298-01