/**
* oj.Collection based implementation of TreeDataSource
* @param {Object} options an object containing the following options:<p>
* root: an oj.Collection specifying the root level Collection<p>
* childCollectionCallback: a function(oj.Collection,oj.Model):oj.Collection callback to return a child collection given a root and model representing the parent<p>
* parseMetadata: a function(oj.Model):Object callback to return key, leaf, depth metadata from a given Model<p>
* @constructor
* @export
* @extends oj.TreeDataSource
*/
oj.CollectionTreeDataSource = function(options)
{
options = options || {};
this.rootCollection = options['root'];
this.childCollectionCallback = options['childCollectionCallback'];
this.parseMetadata = options['parseMetadata'];
this.sortkey = null;
this.sortdir = "none";
// This is a parent->collection cache
this.cache = {};
oj.CollectionTreeDataSource.superclass.constructor.call(this);
};
// Default implementation
oj.CollectionTreeDataSource.prototype.parseMetadata = function(model) {
return {'key':model['idAttribute']+"="+model['id']};
};
// Subclass from oj.TreeDataSource
oj.Object.createSubclass(oj.CollectionTreeDataSource, oj.TreeDataSource, "oj.CollectionTreeDataSource");
/**
* Initializes the data source.
* @export
*/
oj.CollectionTreeDataSource.prototype.Init = function()
{
// super
oj.CollectionTreeDataSource.superclass.Init.call(this);
};
/**
* Returns the number of children for a specified parent. If the value returned is not >= 0 then it is automatically assumed
* that the child count is unknown.
* @param {Object} parent the parent key. Specify null if inquiring child count of the root.
* @return {number} the number of children for the specified parent.
* @export
*/
oj.CollectionTreeDataSource.prototype.getChildCount = function(parent)
{
var childColl = this.cache[parent];
if (childColl) {
if (childColl.length > 0) {
return childColl.length;
}
}
this.getChildCollection(parent, {'success': function(coll) {
return coll.length;
}});
return -1;
};
/**
* Gets a collection representing the specified parent key's children.
* @param {Object} key the parent key in which to create an OJ collection for.
* @param {Object} callbacks success and error callbacks. The success callback will provide the child collection as the first argument
* @export
*/
oj.CollectionTreeDataSource.prototype.getChildCollection = function(key, callbacks) {
this.fetchChildren(key, null, {'success':function(nodeSet) {
callbacks['success'](nodeSet._getCollection());
}, 'error':callbacks['error']});
};
/**
* Fetch the children
* @param {Object} parent the parent key. Specify null if fetching children from the root.
* @param {Object} range information about the range, it must contain the following properties: start, count.<p>
* range.start the start index of the range in which the children are fetched.<p>
* range.count the size of the range in which the children are fetched. <p>
* @param {Object} callbacks the callbacks to be invoke when fetch children operation is completed. The valid callback
* types are "success" and "error".<p>
* {function(oj.NodeSet)} callbacks.success the callback to invoke when fetch completed successfully.<p>
* {function({status: Object})} callbacks.error the callback to invoke when fetch children failed.<p>
* @param {Object=} options optional parameters for this operation. <p>
* {boolean=} options.queueOnly true if this fetch request is to be queued and not execute yet. The implementation must maintain
* the order of the fetch operations. When queueOnly is false/null/undefined, any queued fetch operations are then
* flushed and executed in the order they are queued. This flag is ignored if the datasource does not support batching.
* @export
*/
oj.CollectionTreeDataSource.prototype.fetchChildren = function(parent, range, callbacks, options)
{
range = range || {};
var start = range['start'] ? range['start'] : 0;
var count = range['count'] ? range['count'] : -1;
if (parent === null) {
// Do root
this.FetchCollection(null, start, count, callbacks, null);
return;
}
// Use child collection callback to set up child collection, then fetch it
var self = this;
this._getModelForId(this.rootCollection, parent, 0).then(function(parentModel) {
if (parentModel) {
var collection = self.GetChildCollection(parentModel.model);
try {
self.FetchCollection(collection, start, count, callbacks, parent);
}
catch (error)
{
if (callbacks && callbacks['error']) {
callbacks['error']({'status': error['message']});
}
}
}
else {
// Couldn't find a model for the given Id: call the error if present
if (callbacks && callbacks['error']) {
callbacks['error'](parent);
}
}
});
};
// Called by common model when a model is added to a collection
oj.CollectionTreeDataSource.prototype.ModelAdded = function(model, collection, options) {
var index = 0;
if (options && options['at']) {
index = options['at'];
}
var parents = this._getParentChain(collection);
var parent = parents != null && parents.length > 0 ? parents[parents.length-1] : null;
var event = this._createEvent(this, "insert", index, parents, this._putModelInNodeSet(parent, model));
this.handleEvent("change", event);
};
// Called by common model when a model is removed from a collection
oj.CollectionTreeDataSource.prototype.ModelRemoved = function(model, collection, options) {
var index = 0;
if (options && options['index']) {
index = options['index'];
}
this._removeCollectionFromCache(model);
var event = this._createEvent(this, "delete", index, this._getParentChain(collection), null);
this.handleEvent("change", event);
};
// Called by common model when a model is updated
oj.CollectionTreeDataSource.prototype.ModelUpdated = function(model, options) {
var collectionForModel = this._getCollectionForModel(model);
var index = null;
var parents = null;
if (collectionForModel) {
// If this model's collection is found, get the index where it was found and its parent chain from the actual collection
index = collectionForModel.index;
parents = this._getParentChain(collectionForModel.collection);
}
var parent = parents != null && parents.length > 0 ? parents[parents.length-1] : null;
var event = this._createEvent(this, "update", index, parents, this._putModelInNodeSet(parent, model));
this.handleEvent("change", event);
};
// Called if a collection is refreshed
oj.CollectionTreeDataSource.prototype.CollectionRefreshed = function(collection, resp, options) {
var event = this._createEvent(this, "refresh", null, this._getParentChain(collection), null);
this.handleEvent("refresh", event);
};
oj.CollectionTreeDataSource.prototype._putModelInNodeSet = function(parent, model) {
/* var rows = [];
rows.push(model.attributes);
var options = {};
options['idAttribute'] = model['idAttribute'];
var rowset = new oj.ArrayRowSet(rows, options);
rowset.fetch();
return rowset;*/
var collection = new oj.Collection();
collection.add(model);
return this._getNodeSet(collection, parent, 0, 1);
};
// Return an array of parent keys representing the parentage of the given collection. Relies on cached collection fetches. Array is from
// child key of root on down to collection's parent
oj.CollectionTreeDataSource.prototype._getParentChain = function(collection) {
var parents = [];
var parent = null;
var coll = collection;
do {
parent = this._getParentOfCollection(coll);
if (parent !== null) {
if (parent !== oj.CollectionTreeDataSource.ROOT_CACHE_KEY) {
parents.unshift(parent);
}
coll = this._getCollectionOfKey(parent);
}
} while (parent != null);
return parents;
};
// Generate the cache key
oj.CollectionTreeDataSource.ROOT_CACHE_KEY = "%!@ROOT%#@!";
oj.CollectionTreeDataSource.prototype._getCacheKey = function(model) {
// If model (parent) is null, look for the root
// If model is not an oj.Model, just use it as the key
var key = model instanceof oj.Model ? this.parseMetadata(model)['key'] : model;
return model ? key : oj.CollectionTreeDataSource.ROOT_CACHE_KEY;
};
// Return a cached collection given a parent Model. Null if not cached
oj.CollectionTreeDataSource.prototype.__getParentsChildCollectionFromCache = function(model) {
return this.cache[this._getCacheKey(model)];
};
// Put a collection into the cache whose parent is represented by model
oj.CollectionTreeDataSource.prototype._setCollectionInCache = function(model, collection) {
// Tack on listeners
collection.on(oj.Events.EventType['ADD'], this.ModelAdded, this);
collection.on(oj.Events.EventType['REMOVE'], this.ModelRemoved, this);
collection.on(oj.Events.EventType['CHANGE'], this.ModelUpdated, this);
collection.on(oj.Events.EventType['SYNC'], this.CollectionRefreshed, this);
this.cache[this._getCacheKey(model)] = collection;
};
// Remove any collections with the given model as a parent
oj.CollectionTreeDataSource.prototype._removeCollectionFromCache = function(model) {
var key = this._getCacheKey(model);
for (var prop in this.cache) {
if (this.cache.hasOwnProperty(prop)) {
if (prop === key) {
// Found it: remove listeners and delete it
this.cache[key].off(null, null, this);
delete this.cache[key];
return;
}
}
}
};
// Determine if a model representing a given parent key is found in the given collection
oj.CollectionTreeDataSource.prototype._keyInCollection = function(key, collection) {
var count = collection.length;
for (var i = 0; i < count; i++) {
var currKey = this._getCacheKey(collection.at(i));
if (key === currKey) {
return true;
}
}
return false;
};
// Get the collection and index of the given model
oj.CollectionTreeDataSource.prototype._getCollectionForModel = function(model) {
// Search the cache for the parent key
for (var prop in this.cache) {
if (this.cache.hasOwnProperty(prop)) {
var collection = this.cache[prop];
for (var i = 0; i < collection.length; i++) {
if (collection.at(i) === model) {
return {index:i, collection:collection};
}
}
}
}
return null;
};
// Get the collection containing the given key
oj.CollectionTreeDataSource.prototype._getCollectionOfKey = function(key) {
// Search the cache for the parent key
for (var prop in this.cache) {
if (this.cache.hasOwnProperty(prop)) {
var collection = this.cache[prop];
if (this._keyInCollection(key, collection)) {
return collection;
}
}
}
return null;
};
// Get the parent key for the given collection from the cache, if found
oj.CollectionTreeDataSource.prototype._getParentOfCollection = function(collection) {
// Search the cache for the parent key
for (var prop in this.cache) {
if (this.cache.hasOwnProperty(prop)) {
if (this.cache[prop] === collection) {
return prop;
}
}
}
return null;
};
// Return an object giving the child collection for the given model along with a boolean indicating whether the collection was found
// in cache.
oj.CollectionTreeDataSource.prototype.GetChildCollection = function(parentModel) {
// Is it in the cache?
var cached = true;
var collection = this.__getParentsChildCollectionFromCache(parentModel);
if (!collection) {
// Nothing found at all yet
cached = false;
// Ask the app for the child collection for the given model
collection = this.childCollectionCallback(this.rootCollection, parentModel);
if (collection != null) {
// Make sure we transfer sorting properties...
this._applySortToCollection(collection);
// And put it in the cache
this._setCollectionInCache(parentModel, collection);
}
}
return {collection:collection,cached:cached};
};
oj.CollectionTreeDataSource.prototype._createEvent = function(source, operation, index, parent, data) {
return {'source':source, 'operation':operation, 'index':index, 'parent':parent, 'data':data};
};
// Call to fetch models for an already-created collection
oj.CollectionTreeDataSource.prototype.FetchCollection = function(collection, start, count, callbacks, parent) {
var self = this;
if (collection === null) {
// Fetch the root
// Check for the root in the cache
collection = this.__getParentsChildCollectionFromCache(null);
if (!collection) {
// Not found, put a representation in there
collection = {collection:self.rootCollection,cached:false};
self._setCollectionInCache(null, this.rootCollection);
}
else {
// Root was found precached: set up its information
collection = {collection:collection, cached:true};
}
}
if (collection) {
self._fetch(collection, function (coll) {
if (callbacks['success']) {
// return a nodeset version of this fetched collection via the callback
callbacks['success'](self._getNodeSet(coll, parent, start, count));
}
}, callbacks['error']);
}
};
// Turn collection into a CollectionNodeSet
oj.CollectionTreeDataSource.prototype._getNodeSet = function(collection, parent, start, count) {
return new oj.CollectionNodeSet(parent, collection, this, start, count);
};
// Do any of the models in this collection or its children have the given key
oj.CollectionTreeDataSource.prototype._scanForKey = function(collection, key) {
// Search collection for key
var self = this;
return new Promise(function(resolve, reject) {
function checkNext(index, collection, key) {
if (index < collection.length) {
// Get the model at location index
collection.at(index, {'deferred':true}).then(function (model) {
// Does the retrieved model match the key when parsed out? If so return it
if (model) {
var parse = self.parseMetadata(model);
if (key === parse['key']) {
resolve(model);
return;
}
}
// No model or no match: go to the next model in the collection
index++;
checkNext(index, collection, key);
});
}
else {
// Hit the end: not found
resolve(null);
return;
}
};
// Start checking at the 0th model in the collection
checkNext(0, collection, key);
});
};
// Returns a promise with an object giving the model that corresponds to the given key, and the depth in the hierarchy at which it was found
oj.CollectionTreeDataSource.prototype._getModelForId = function(collection, key, depth) {
var self = this;
return new Promise(function(resolve, reject) {
// Does the starting collection contain the key?
self._scanForKey(collection, key).then(function (model) {
// Yes, resolve (return it)
if (model) {
resolve({model:model, depth: depth});
return;
}
// It does not
// Wrong collection: search each model's child collections, and check recursively
var max = collection.length;
// Check each model in collection now for key
function getNextCollection(index, tds) {
if (index < max) {
// Return an object containing the child collection of the current collection + whether its cached
var childColl = tds.GetChildCollection(collection.at(index));
if (!childColl.collection) {
// No child collection found: move on to the next model in the current collection
index++;
getNextCollection(index, tds);
} else {
// Fetch the child collection if necessary
tds._fetch(childColl, function(fetchColl) {
// Now check the child collection recursively for the key, moving depth down 1
tds._getModelForId(fetchColl, key, depth+1).then(function(childModel) {
// Found the model in this collection: unwind
if (childModel) {
// Found somewhere down this tree
resolve(childModel);
return;
} else {
// Not found down that tree
// Move to the next model
index++;
getNextCollection(index, tds);
}
});
}, null);
}
}
else {
// Hit the end
resolve(null);
}
}
// Start checking the collection at the 0th model
getNextCollection(0, self);
});
});
};
// Do a fetch or just return the collection if it came from cache
oj.CollectionTreeDataSource.prototype._fetch = function(collectionCacheObj, success, error) {
var cached = collectionCacheObj.cached;
if (cached) {
// If it's cached, it's fetched
success(collectionCacheObj.collection);
}
else {
// apply sorting if necessary
if (this.sortkey && this.sortkey !== "none") {
collectionCacheObj.collection.comparator = this.sortkey;
collectionCacheObj.collection.sortDirection = this.sortdir;
}
if (collectionCacheObj.collection.length > 0 || !collectionCacheObj.collection.IsUrlBased(null)) {
// Already fetched: just return
success(collectionCacheObj.collection);
return;
}
// Do a real fetch from the collection's url
collectionCacheObj.collection.fetch({'success':function(fetchColl) {
// Tack on handlers
success(fetchColl);
}, 'error':error});
}
};
/**
* Fetch all children and their children recursively from a specified parent.
* @param {Object} parent the parent key. Specify null to fetch everything from the root (i.e. expand all)
* @param {Object} callbacks the callbacks to be invoke when fetch children operation is completed. The valid callback
* types are "success" and "error".<p>
* {function(oj.CollectionNodeSet)} success the callback to invoke when fetch completed successfully.<p>
* {function({status: Object})} error the callback to invoke when fetch children failed.<p>
* @param {Object=} options optional parameters for this operation.<p>
* {number=} start the index related to parent in which to begin fetching descendants from. If this is not specified, then <p>
* {number=} maxCount the maximum number of children to fetch. If a non-positive number is specified, then the value is ignored and
* there is no maximum fetch count.
* @export
*/
oj.CollectionTreeDataSource.prototype.fetchDescendants = function(parent, callbacks, options)
{
var self = this;
if (parent === null) {
// Do root
this.FetchCollection(null, 0, -1, {'success':function(nodeSet) {
nodeSet.FetchDescendants({'success':function() {
if (callbacks['success']) {
callbacks['success'](nodeSet);
}
}});
}}, null);
return;
}
// Use child collection callback to set up child collection, then fetch it
this._getModelForId(this.rootCollection, parent, 0).then(function(parentModel) {
if (parentModel) {
var collection = self.GetChildCollection(parentModel.model);
self.FetchCollection(collection, 0, -1, {'success':function(nodeSet) {
nodeSet.FetchDescendants({'success':function() {
if (callbacks['success']) {
callbacks['success'](nodeSet);
}
}});
}}, parent);
}
});
};
/**
* Performs a sort operation on the tree data.
* @param {Object} criteria the sort criteria. It must contain the following properties: key, direction<p>
* criteria.key the key identifying the attribute (column) to sort on<p>
* criteria.direction the sort direction, valid values are "ascending", "descending", "none" (default)<p>
* @param {function({status: Object})} callbacks.success the callback to invoke when the sort completed successfully.<p>
* callbacks.error the callback to invoke when sort failed.
* @export
*/
oj.CollectionTreeDataSource.prototype.sort = function(criteria, callbacks)
{
var key = criteria['key'];
var dir = criteria['direction'];
var needSort = false;
if (key !== this.sortkey) {
this.sortkey = key;
needSort = true;
}
if (dir !== this.sortdir) {
this.sortdir = dir;
needSort = true;
}
if (needSort) {
// If clearing, clear the cache
if (this.sortdir === "none") {
this.cache = {};
}
// Go off and sort each collection, as long as it has "criteria" as one of its attributes
for (var prop in this.cache) {
if (this.cache.hasOwnProperty(prop)) {
var collection = this.cache[prop];
this._applySortToCollection(collection);
}
}
}
if (callbacks && callbacks['success']) {
callbacks['success']();
}
};
oj.CollectionTreeDataSource.prototype._applySortToCollection = function(collection) {
collection['comparator'] = this.sortkey;
collection['sortDirection'] = (this.sortdir === "ascending") ? 1 : -1;
collection.sort();
};
/**
* Returns the current sort criteria of the tree data.
* @return {Object} the current sort criteria. It should contain the following properties: key, direction where
* criteria.key the key identifying the attribute (column) to sort on. Value is null if it's not sorted.
* criteria.direction the sort direction, valid values are "ascending", "descending", "none" (default)
* @export
*/
oj.CollectionTreeDataSource.prototype.getSortCriteria = function()
{
return {'key': this.sortkey, 'direction': this.sortdir};
};
/**
* Moves a row from one location to another (different position within the same parent or a completely different parent)
* @param {Object} rowToMove the key of the row to move
* @param {Object} referenceRow the key of the reference row which combined with position are used to determine
* the destination of where the row should moved to.
* @param {number|string} position The position of the moved row relative to the reference row.
* This can be a string: "before", "after", "inside", "first", "last", or the zero based index to position
* the element at a specific point among the reference row's current children.
* @param {function()} callbacks.success the callback to invoke when the move completed successfully.
* @param {function({status: Object})} callbacks.error the callback to invoke when move failed.
* @export
*/
oj.CollectionTreeDataSource.prototype.move = function(rowToMove, referenceRow, position, callbacks)
{
oj.Assert.failedInAbstractFunction();
};
/**
* Checks whether a move operation is valid.
* @param {Object} rowToMove the key of the row to move
* @param {Object} referenceRow the key of the reference row which combined with position are used to determine
* the destination of where the row should moved to.
* @param {number|string} position The position of the moved row relative to the reference row.
* This can be a string: "before", "after", "inside", "first", "last", or the zero based index to position
* the element at a specific point among the reference row's current children.
* @return {string} returns "valid" if the move is valid, "invalid" otherwise.
* @export
*/
oj.CollectionTreeDataSource.prototype.moveOK = function(rowToMove, referenceRow, position)
{
return "invalid";
};
/**
* Determines whether this CollectionTreeDataSource supports the specified feature.
* @param {string} feature the feature in which its capabilities is inquired. Currently the valid features "sort",
* "move", "fetchDescendants", "batchFetch"
* @return {string|null} the name of the feature. Returns null if the feature is not recognized.
* For "sort", the valid return values are: "default", "none".
* For "fetchDescendants", the valid return values are: "enable", "disable", "suboptimal".
* For "move", the valid return values are: "default", "none".
* For "batchFetch", the valid return values are: "enable", "disable".
* @export
*/
oj.CollectionTreeDataSource.prototype.getCapability = function(feature)
{
if (feature === "sort") {
return "default";
}
if (feature === "move") {
return "none";
}
if (feature === "batchFetch") {
return "disable";
}
if (feature === "fetchDescendants") {
return "disable";
}
return null;
};
Source: src/main/javascript/oracle/oj/ojtree-model/CollectionTreeDataSource.js
Oracle® JavaScript Extension Toolkit (JET)
1.1.2
E65298-01