Source: src/main/javascript/oracle/oj/ojdatagrid-model/CollectionDataGridDataSource.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2014, Oracle and/or its affiliates.
 * All rights reserved.
 */
 
/**
 * An OJ Collection based implementation of the DataGridDataSource.
 * @param {Object} collection the oj collection to adapter the DataGridDataSource
 * @param {Object=} options optional settings on this oj collection data source
 * @param {string=} options.rowHeader the key of the attribute designated as the row header
 * @param {Array.<string>=} options.columns explicitly specifies columns to display and in 
 *        what order.  These columns must be a subset of attributes from Model.
 * @export
 * @constructor
 * @extends oj.DataGridDataSource
 */
oj.CollectionDataGridDataSource = function(collection, options)
{
    this.collection = collection;
    if (options != null)
    {
        this.rowHeader = options['rowHeader'];
        this.columns = options['columns'];
    }

    oj.CollectionDataGridDataSource.superclass.constructor.call(this);
};

//subclass of DataGridDataSource
oj.Object.createSubclass(oj.CollectionDataGridDataSource, oj.DataGridDataSource, "oj.CollectionDataGridDataSource");

/**
 * Initial the OJ collection based data source.
 * @export
 */
oj.CollectionDataGridDataSource.prototype.Init = function()
{
    // call super
    oj.CollectionDataGridDataSource.superclass.Init.call(this);

    this.pendingHeaderCallback = {};
    this._registerEventListeners();
};

/**
 * Register event handlers on the underlying OJ collection.
 * @private
 */
oj.CollectionDataGridDataSource.prototype._registerEventListeners = function()
{
    this.collection.on("add", this._handleModelAdded.bind(this));
    this.collection.on("remove", this._handleModelDeleted.bind(this));
    this.collection.on("change", this._handleModelChanged.bind(this));
    this.collection.on("refresh", this._handleCollectionRefresh.bind(this));
    this.collection.on("reset", this._handleCollectionReset.bind(this));
};

/**
 * Determines if data has been fetched if virtual
 * @return {boolean} true if data is locally available, false otherwise.
 * @private
 */
oj.CollectionDataGridDataSource.prototype._isDataAvailable = function()
{
    return (this.data != null);
};

/**
 * Returns the total number of rows or columns.  If the value return is not >= 0 then it is automatically assumed
 * that the total count is unknown. In the case of paging returns the total number of rows/colums on the page.
 * @param {string} axis the axis in which we inquire for the total count.  Valid values are "row" and "column".
 * @return {number} the total number of rows/columns.
 * @export
 */
oj.CollectionDataGridDataSource.prototype.getCount = function(axis)
{
    var totalSize;
    
    if (this.precision == undefined)
    {
        this.precision = {};
    }

    if (axis == "row")
    {
        totalSize = this._totalSize();
        // 1. totalResults undefined
        // 2. data hasn't been fetched yet and totalResults set to 0 on collection
        // 3. the collection has size but not totalResults, truly unkown count
        if (totalSize === -1 || (totalSize === 0  && (!this._isDataAvailable() || this._size() > 0)))
        {
            this.precision[axis] = "estimate";            
            return -1;
        }
        // otherwise we will return the size of the collection or the pageSize
        this.precision[axis] = "exact";
        return this._size();
    }
    if (axis == "column")
    {
        // check whether column info are available
        if (this.columns != null)
        {
            this.precision[axis] = "exact";
            return this.columns.length;
        }
        else
        {
            this.precision[axis] = "estimate";
            return -1;
        }
    }

    // should not get here
    return 0;
};
/**
 * Returns whether the total count returned in getCount function is an actual or an estimate.
 * @param {string} axis the axis in which we inquire whether the total count is an estimate.  Valid values are 
 *        "row" and "column".
 * @return {string} "actual" if the count returned in getCount function is the actual count, "estimate" if the 
 *         count returned in getCount function is an estimate.  The default value is "actual".
 * @export
 */
oj.CollectionDataGridDataSource.prototype.getCountPrecision = function(axis)
{
    // if precision has not been determine for the axis, invoke getCount
    if (this.precision == null || this.precision[axis] == null)
    {
        this.getCount(axis);
    }
    return this.precision[axis];
};

/**
 * Fetch a range of headers from the data source.
 * @param {Object} headerRange information about the header range, it must contain the following properties:
 *        axis, start, count.
 * @param {string} headerRange.axis the axis of the header that are fetched.  Valid values are "row" and "column".
 * @param {number} headerRange.start the start index of the range in which the header data are fetched.
 * @param {number} headerRange.count the size of the range in which the header data are fetched.  
 * @param {Object} callbacks the callbacks to be invoke when fetch headers operation is completed.  The valid callback
 *        types are "success" and "error".
 * @param {function(HeaderSet)} callbacks.success the callback to invoke when fetch headers completed successfully.
 * @param {function({status: Object})} callbacks.error the callback to invoke when fetch cells failed.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" keys.
 * @export
 */
oj.CollectionDataGridDataSource.prototype.fetchHeaders = function(headerRange, callbacks, callbackObjects)
{
    var axis, callback;
    // still fetching, just store the callback info
    if (callbacks != null)
    {
        axis = headerRange['axis'];
        callback = {};
        callback.headerRange = headerRange;
        callback.callbacks = callbacks;
        callback.callbackObjects = callbackObjects;
        this.pendingHeaderCallback[axis] = callback;
    }
};

/**
 * Handle success fetchHeaders request
 * @param {Object} headerRange information about the header range, it must contain the following properties:
 *        axis, start, count.
 * @param {string} headerRange.axis the axis of the header that are fetched.  Valid values are "row" and "column".
 * @param {number} headerRange.start the start index of the range in which the header data are fetched.
 * @param {number} headerRange.count the size of the range in which the header data are fetched.  
 * @param {Object} callbacks the callbacks to be invoke when fetch headers operation is completed.  The valid callback
 *        types are "success" and "error".
 * @param {function(HeaderSet)} callbacks.success the callback to invoke when fetch headers completed successfully.
 * @param {function({status: Object})} callbacks.error the callback to invoke when fetch cells failed.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" keys.
 * @param {Object=} actualRange the count and start returned from the server
 * @param {number} actualRange.start the start index of the data the server returned
 * @param {number} actualRange.count the size of the range the server returned
 * @private 
 */
oj.CollectionDataGridDataSource.prototype._handleHeaderFetchSuccess = function(headerRange, callbacks, callbackObjects, actualRange)
{
    var axis, start, count, end, headerSet;

    axis = headerRange.axis;
    start = headerRange.start;
    count = headerRange.count;	                

    oj.Assert.assert(axis === 'row' || axis === 'column');    
    oj.Assert.assert(count > 0);
	      
    if (axis === "column")
    {  
        // column headers, this.columns should be populated by now
        if (this.columns != null)
        {
            end = Math.min(this.columns.length, start+count);
            headerSet = new oj.CollectionHeaderSet(start, end, this.columns);
        }
        else
        {
            // no row header, return empty result set
            headerSet = new oj.ArrayHeaderSet(start, start, axis, null);
        }
    }
    else if (axis === "row")
    {
        // row headers, return non-empty header set if row header is specified
        if (this.rowHeader != null)
        {
            //need the actual rows that the server returned to create the header set
            if (actualRange != null)
            {
                count = actualRange.count;	                
            }
            end = Math.min(this._size(), start+count);

            headerSet = new oj.CollectionHeaderSet(start, end, this.columns, this.rowHeader);     
            // resolve any promises before invoking callbacks
            this._resolveModels(start, end, headerSet, headerRange, callbacks, callbackObjects);

            // resolveModels will invoke callbacks
            return;
        }
        else
        {
            // no row header, return empty result set
            headerSet = new oj.ArrayHeaderSet(start, start, axis, null);
        }
    }

    // invoke callback
    if (callbacks != null && callbacks['success'])
    {
        callbacks['success'].call(callbackObjects['success'], headerSet, headerRange);
    }
};

/**
 * Helper method to extract range information from cellRanges
 * @param {Array.<Object>} cellRanges Information about the cell range.  A cell range is defined by an array 
 *        of range info for each axis, where each range contains three properties: axis, start, count.
 * @param {string} cellRanges.axis the axis associated with this range where cells are fetched.  Valid 
 *        values are "row" and "column".
 * @param {number} cellRanges.start the start index of the range for this axis in which the cells are fetched.
 * @param {number} cellRanges.count the size of the range for this axis in which the cells are fetched. 
 * @return {Object} an object containing rowStart, rowCount, colStart, colCount
 * @private
 */
oj.CollectionDataGridDataSource.prototype._getRanges = function(cellRanges)
{
    var i, cellRange, rowStart, rowCount, colStart, colCount;

    // extract the start and end row/column info from cellRanges (there should only be two, one for each axis)
    for (i=0; i<cellRanges.length; i+=1)
    {
        cellRange = cellRanges[i];   
        oj.Assert.assert(cellRange['axis'] === 'row' || cellRange['axis'] === 'column');
        oj.Assert.assert(cellRange['count'] > 0);
        if (cellRange['axis'] === "row")
        {
            rowStart = cellRange['start'];
            rowCount = cellRange['count'];
        }
        else if (cellRange['axis'] === "column")
        {
            colStart = cellRange['start'];
            colCount = cellRange['count'];
        }
    }			

    // return object containing the ranges
    return {'rowStart': rowStart, 'rowCount': rowCount, 'colStart': colStart, 'colCount': colCount};
};

/**
 * Handle success fetchCells request
 * @param {Array.<Object>} cellRanges Information about the cell range.  A cell range is defined by an array 
 *        of range info for each axis, where each range contains three properties: axis, start, count.
 * @param {string} cellRanges.axis the axis associated with this range where cells are fetched.  Valid 
 *        values are "row" and "column".
 * @param {number} cellRanges.start the start index of the range for this axis in which the cells are fetched.
 * @param {number} cellRanges.count the size of the range for this axis in which the cells are fetched. 
 * @param {Object} callbacks the callbacks to be invoke when fetch cells operation is completed.  The valid callback
 *        types are "success" and "error".
 * @param {function(CellSet)} callbacks.success the callback to invoke when fetch cells completed successfully.
 * @param {function({status: Object})} callbacks.error the callback to invoke when fetch cells failed.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" keys.
 * @param {Object=} actualRange the count and start returned from the server
 * @param {number} actualRange.start the start index of the data the server returned
 * @param {number} actualRange.count the size of the range the server returned        
 * @private
 */
oj.CollectionDataGridDataSource.prototype._handleCellFetchSuccess = function(cellRanges, callbacks, callbackObjects, actualRange)
{
    var ranges, rowStart, rowEnd, colStart, colEnd, cellSet;

    // extract the start and end row/column info from cellRanges (there should only be two, one for each axis)
    ranges = this._getRanges(cellRanges);
    rowStart = ranges['rowStart'];
    //use the actual rows returned by the server
    if (actualRange != null)
    {
        rowEnd = Math.min(this._size(), rowStart + actualRange['count']);
    }
    else
    {
        rowEnd = Math.min(this._size(), rowStart + ranges['rowCount']);        
    }
    colStart = ranges['colStart'];
    //if there are no columns make sure we return an empty cellSet
    colEnd = Math.min(this.columns == null ? 0 : this.columns.length, colStart + ranges['colCount']);

    // resolves models at row range
    cellSet = new oj.CollectionCellSet(rowStart, rowEnd, colStart, colEnd, this.columns);
    this._resolveModels(rowStart, rowEnd, cellSet, cellRanges, callbacks, callbackObjects);
};

/**
 * Resolves all the promises from Collection before invoking callbacks
 * @param {number} rowStart the start row index in the cell set
 * @param {number} rowEnd the end row index in the cell set
 * @param {Object} set the result HeaderSet or CellSet that is return to callbacks when models are resolved
 * @param {Array.<Object>|Object} ranges Information about the header/cell range.  
 * @param {function(Object)} callbacks.success the callback to invoke when fetch headers/cells completed successfully.
 * @param {function({status: Object})} callbacks.error the callback to invoke when fetch headers/cells failed.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" keys.
 * @private
 */
oj.CollectionDataGridDataSource.prototype._resolveModels = function(rowStart, rowEnd, set, ranges, callbacks, callbackObjects)
{
    var promises, i;

    promises = [];
    for (i = rowStart; i < rowEnd; i++) 
    {
        promises.push(this.collection.at(i, {'deferred':true}));
    }
    
    // resolves all promises
    Promise.all(promises).then(function(models) 
    {
        // all promises resolved, invoke callback with cell/header set
        set.setModels(models);
        callbacks['success'].call(callbackObjects['success'], set, ranges);
    });
};

/**
 * Fetch a range of cells from the data source.
 * @param {Array.<Object>} cellRanges Information about the cell range.  A cell range is defined by an array 
 *        of range info for each axis, where each range contains three properties: axis, start, count.
 * @param {string} cellRanges.axis the axis associated with this range where cells are fetched.  Valid 
 *        values are "row" and "column".
 * @param {number} cellRanges.start the start index of the range for this axis in which the cells are fetched.
 * @param {number} cellRanges.count the size of the range for this axis in which the cells are fetched. 
 * @param {Object} callbacks the callbacks to be invoke when fetch cells operation is completed.  The valid callback
 *        types are "success" and "error".
 * @param {function(CellSet)} callbacks.success the callback to invoke when fetch cells completed successfully.
 * @param {function({status: Object})} callbacks.error the callback to invoke when fetch cells failed.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" keys.
 * @export
 */
oj.CollectionDataGridDataSource.prototype.fetchCells = function(cellRanges, callbacks, callbackObjects)
{
    // still fetching, just store the callback info
    if (callbacks != null)
    {
        this.pendingCellCallback = {};
        this.pendingCellCallback.cellRanges = cellRanges;
        this.pendingCellCallback.callbacks = callbacks;
        this.pendingCellCallback.callbackObjects = callbackObjects;
    }

    // kick start a setRangeLocal call on the collection
    this._fetchCells(cellRanges);
};

/**
 * Processing pending header callbacks.
 * @param {string} axis the axis to check for pending header callbacks.
 * @private
 */
oj.CollectionDataGridDataSource.prototype._processPendingHeaderCallbacks = function(axis)
{
    var pendingCallback, headerRange, callbacks, callbackObjects, actualRange;

    // check if there's callback remaining for the axis
    pendingCallback = this.pendingHeaderCallback[axis];
    if (pendingCallback != null)
    {
        // todo: check whether pending header range matches result
        headerRange = pendingCallback.headerRange;
        callbacks = pendingCallback.callbacks;
        callbackObjects = pendingCallback.callbackObjects;
        if (axis === "row")
        {
            actualRange = pendingCallback.actualRange;
        }
        this._handleHeaderFetchSuccess(headerRange, callbacks, callbackObjects, actualRange);
            
        // clear any pending callback
        this.pendingHeaderCallback[axis] = null;
    } 
};

/**
 * Processing pending cell callbacks.
 * @private
 */
oj.CollectionDataGridDataSource.prototype._processPendingCellCallbacks = function()
{
    var cellRanges, callbacks, callbackObjects, actualRange;

    cellRanges = this.pendingCellCallback.cellRanges;
    callbacks = this.pendingCellCallback.callbacks;
    callbackObjects = this.pendingCellCallback.callbackObjects;
    actualRange = this.pendingCellCallback.actualRange;
    // handles success cell fetch
    this._handleCellFetchSuccess(cellRanges, callbacks, callbackObjects, actualRange);
};

/**
 * Internal method to handle fetching of cells for virtualized collection.
 * @param {Array.<Object>} cellRanges Information about the cell range.  A cell range is defined by an array 
 *        of range info for each axis, where each range contains three properties: axis, start, count.
 * @param {string} cellRanges.axis the axis associated with this range where cells are fetched.  Valid 
 *        values are "row" and "column".
 * @param {number} cellRanges.start the start index of the range for this axis in which the cells are fetched.
 * @param {number} cellRanges.count the size of the range for this axis in which the cells are fetched. 
 * @private
 */
oj.CollectionDataGridDataSource.prototype._fetchCells = function(cellRanges)
{
    var ranges, rowStart, rowCount;

    ranges = this._getRanges(cellRanges);
    rowStart = ranges['rowStart'];
    rowCount = ranges['rowCount'];

    // set the range local for the requested range
    this.collection.setRangeLocal(rowStart, rowCount).then(function (actual)
    {
        // keeps track of whether or not the data source has fetched
        this.data = true;
        
        var first = this.collection.at(rowStart, {'deferred':true});
        
        this._setActualCallbackRanges(actual.start, actual.count);
        
        // check if we need to poach columns from row
        if (first != null && this.columns === undefined)
        {
            first.then(function(model)
            {
                this._setupColumns(model);
                // now we can complete the fetch
                this._fetchCellsComplete();
            }.bind(this));

            // process must be done after columns are discovered
            return;
        }
        else
        {
            this._fetchCellsComplete();
        }       
    }.bind(this), function (e)
    {
        this._fetchCellsError(e);
    }.bind(this));
};

/**
 * Handles error from setRangeLocal call in fetchCells.
 * @param {Object} error the error returned from setRangeLocal error callback.
 * @private
 */
oj.CollectionDataGridDataSource.prototype._fetchCellsError = function(error)
{
    // log the error
    oj.Logger.error(error);

    // invoke any header error callbacks
    if (this.pendingHeaderCallback != null)
    {
        this._processPendingHeaderErrorCallbacks('column', error);    
        this._processPendingHeaderErrorCallbacks('row', error);
    }

    // invoke any cell error callbacks
    if (this.pendingCellCallback != null)
    {
        this._processPendingCellErrorCallbacks(error);
    }
};

/** 
 * Handles any pending header callbacks in the case of error being thrown.
 * @param {string} axis the header axis
 * @param {Object} error the error returned from setRangeLocal error callback
 * @private
 */
oj.CollectionDataGridDataSource.prototype._processPendingHeaderErrorCallbacks = function(axis, error)
{
    var pendingCallback, callbacks, callbackObjects, headerRange;

    pendingCallback = this.pendingHeaderCallback[axis];
    if (pendingCallback != null)
    {
        callbacks = pendingCallback.callbacks;
        callbackObjects = pendingCallback.callbackObjects;
        headerRange = pendingCallback.headerRange;

        // invoke error callback
        if (callbacks['error'])
        {
            callbacks['error'].call(callbackObjects['error'], error, headerRange);
        }
            
        // clear any pending callback
        this.pendingHeaderCallback[axis] = null;
    } 
};

/** 
 * Handles any pending cell callbacks in the case of error being thrown.
 * @param {Object} error the error returned from setRangeLocal error callback
 * @private
 */
oj.CollectionDataGridDataSource.prototype._processPendingCellErrorCallbacks = function(error)
{
    var callbacks, callbackObjects, cellRanges;

    callbacks = this.pendingCellCallback.callbacks;
    callbackObjects = this.pendingCellCallback.callbackObjects;
    cellRanges = this.pendingCellCallback.cellRanges;

    // invoke error callback
    if (callbacks['error'])
    {
        callbacks['error'].call(callbackObjects['error'], error, cellRanges);
    }

    // clear any pending callback
    this.pendingCellCallback = null;
};

/**
 * Finish fetch cells operation
 * @private
 */
oj.CollectionDataGridDataSource.prototype._fetchCellsComplete = function()
{
    // check outstanding header calls
    if (this.pendingHeaderCallback != null)
    {
        this._processPendingHeaderCallbacks('column');    
        this._processPendingHeaderCallbacks('row');
    }        

    // finally process outstanding cell calls
    if (this.pendingCellCallback != null)
    {          
        this._processPendingCellCallbacks();
    }	
};

/**
 * Takes the actual result start and count from the server and adds it to the pending callbcak objects
 * as the attribute actualRange
 * @param {number} start the start index from the server
 * @param {number} count the count of records from the server
 * @private
 */
oj.CollectionDataGridDataSource.prototype._setActualCallbackRanges = function(start, count)
{
    var actualRange = {'start':start, 'count':count};
    
    if (this.pendingHeaderCallback['row'] != null)
    {
        this.pendingHeaderCallback['row'].actualRange = actualRange; 
    }
    
    if (this.pendingCellCallback != null)
    {
        this.pendingCellCallback.actualRange = actualRange;
    }
};

oj.CollectionDataGridDataSource.prototype._setupColumns = function(model) 
{
    this.columns = model.keys();
    if (this.columns.indexOf(this.rowHeader) != -1)
    {
        this.columns.splice(this.columns.indexOf(this.rowHeader),1);
    }
};

/**
 * Returns the keys based on the indexes. 
 * @param {Object} indexes the index for each axis
 * @param {Object} indexes.row the index for the row axis
 * @param {Object} indexes.column the index for the column axis
 * @return {Object} a Promise object which upon resolution will pass in an object containing the keys for each axis
 * @export
 */
oj.CollectionDataGridDataSource.prototype.keys = function(indexes)
{
    var rowIndex, columnIndex, rowKey, columnKey, atPromise, self;
    rowIndex = indexes['row'];
    columnIndex = indexes['column'];
    self = this;

    return new Promise(function(resolve, reject) {
        atPromise = self.collection.at(rowIndex, {deferred: true});
        //.at() will return null if index known to be OOB
        if (atPromise != null)
        {
            atPromise.then(function(rowModel) {
                rowKey = oj.CollectionDataGridUtils._getModelKey(rowModel);
                if (self.columns == null)
                {
                    self._setupColumns(rowModel);
                }
                columnKey = self.columns[columnIndex];
                resolve({"row": rowKey == null ? null : rowKey, "column": columnKey == null ? null : columnKey});
            }.bind(self));
        }
        else
        {
            resolve({"row": null , "column": null});
        }
    });
};

/**
 * Returns the row and column index based on the keys.
 * @param {Object} keys the key for each axis
 * @param {Object} keys.row the key for the row axis
 * @param {Object} keys.column the key for the column axis
 * @return {Object} a Promise object which upon resolution will pass in an object containing the indexes for each axis
 * @export
 */
oj.CollectionDataGridDataSource.prototype.indexes = function(keys)
{
    var rowKey, columnKey, columnIndex, self;
    rowKey = keys['row']; 
    columnKey = keys['column'];
    self = this;
    
    return new Promise(function(resolve, reject) 
    {
        self.collection.indexOf(rowKey, {deferred:true}).then(function(rowIndex)
        {
            if (self.columns == null)
            {
                self.collection.first(1, {'deferred':true}).then(function(model)
                {
                    self._setupColumns(model);
                    columnIndex = self.columns.indexOf(columnKey);            
                    resolve({"row": rowIndex, "column": columnIndex});                    
                }.bind(self));
            }
            else
            {
                columnIndex = self.columns.indexOf(columnKey);
                resolve({"row": rowIndex, "column": columnIndex});
            }            
        }.bind(self));
    });
};

/**
 * Determines whether this DataGridDataSource supports certain feature.
 * @param {string} feature the feature in which its capabilities is inquired.  Currently the only valid feature is "sort".
 * @return {string|null} the name of the feature.  For sort, the valid return values are: "full", "none".  Returns null if the
 *         feature is not recognized.
 * @export
 */
oj.CollectionDataGridDataSource.prototype.getCapability = function(feature)
{
    if (feature === 'sort')
    {
        // OJ collection based data source supports column sorting only
        return 'column';
    }
    else if (feature === 'move')
    {
        // OJ collection based data source supports row moving only
        return 'row';        
    }
    return null;
};

/**
 * Performs a sort on the data source.
 * @param {Object} criteria the sort criteria. 
 * @param {string} criteria.axis The axis in which the sort is performed, valid values are "row", "column"
 * @param {Object} criteria.key The key that identifies which header to sort
 * @param {string} criteria.direction the sort direction, valid values are "ascending", "descending", "none" (default)
 * @param {Object} callbacks the callbacks to be invoke upon completion of the sort operation.  The callback
 *        properties are "success" and "error".
 * @param {function()} callbacks.success the callback to invoke when the sort completed successfully.  
 * @param {function({status: Object})} callbacks.error the callback to invoke when sort failed.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" properties.
 * @export
 */
oj.CollectionDataGridDataSource.prototype.sort = function(criteria, callbacks, callbackObjects)
{
    var comparator, direction, key, axis;

    // make sure callbackObjects is not null
    if (callbackObjects == null)
    {
        callbackObjects = {};
    }

    // reset sort order if null is specified
    if (criteria == null)
    {
        this._resetSortOrder(callbacks, callbackObjects);
        return;
    }

    direction = criteria['direction'];
    key = criteria['key'];
    axis = criteria['axis'];

    if (axis === "column") {
        //check to see if collection is virtual, if so set the comparator and direction
        if (this.collection.IsVirtual())
        {
            this.collection['comparator'] = key;
            if (direction === 'ascending') 
            {
                this.collection['sortDirection'] = 1;
            }
            else
            {
                this.collection['sortDirection'] = -1;                
            }
        }
        else
        {
            //if the collection is local supply a comparator to allow date sorting
            if (direction === 'ascending') {
                comparator = function(a, b) {
                    var as, bs;
                    //Get the values from the model objects
                    a = a.get(key);
                    b = b.get(key);
                    //Strings of numbers return false, so we can compare strings of numebers with numbers                
                    as = isNaN(a);
                    bs = isNaN(b);
                    //If they dates, turn them into sortable strings         
                    if (a instanceof Date) {
                        a = a.toISOString();
                        as = true;
                    }
                    if (b instanceof Date) {
                        b = b.toISOString();
                        bs = true;
                    }
                    //both are string
                    if (as && bs)
                    {
                        return a < b ? -1 : a === b ? 0 : 1;
                    }
                    //only a is a string
                    if (as)
                    {
                        return 1;
                    }
                    //only b is a string
                    if (bs)
                    {
                        return -1;
                    }
                    //both are numbers
                    return a - b;
                };
            }
            if (direction === 'descending') {
                comparator = function(a, b) {
                    var as, bs;
                    a = a.get(key);
                    b = b.get(key);
                    as = isNaN(a);
                    bs = isNaN(b); 
                    if (a instanceof Date) {
                        a = a.toISOString();
                    }
                    if (b instanceof Date) {
                        b = b.toISOString();
                    }
                    if (as && bs)
                    {
                        return a > b ? -1 : a === b ? 0 : 1;
                    }
                    if (as)
                    {
                        return -1;
                    }
                    if (bs)
                    {
                        return 1;
                    }
                    return b - a;
                };
            }                
            this.collection['comparator'] = comparator;                
        }

        this.collection.sort();

        if (callbacks != null && callbacks['success'] != null)
        {
            callbacks['success'].call(callbackObjects['success']);
        }
    }
    else
    { 
        if (callbacks != null && callbacks['error'] != null)
        {
            callbacks['error'].call(callbackObjects['error'], "Axis value not supported");
        }
    }
};

/**
 * Reset the sort order of the data.
 * @param {Object} callbacks the callbacks to be invoke upon completion of the sort operation.  The callback
 *        properties are "success" and "error".
 * @param {function()} callbacks.success the callback to invoke when the sort completed successfully.  
 * @param {function({status: Object})} callbacks.error the callback to invoke when sort failed.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" properties.
 * @private
 */
oj.CollectionDataGridDataSource.prototype._resetSortOrder = function(callbacks, callbackObjects)
{
    // clear out comparator to reset sort order
    this.collection['comparator'] = null;
    this.collection.reset();

    if (callbacks != null && callbacks['success'] != null)
    {
        callbacks['success'].call(callbackObjects['success']);
    }
};

/**
 * Move a model to a new index in the collection, if atKey is null adds to the end
 * @param {Object} moveKey the key of the model that should be moved
 * @param {Object} atKey the key of the model that the moved model should be inserted before or after
 * @param {string} position The position of the moved row relative to the reference row.  
 *        Valid values are: "before", "after" 
 * @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.
 * @param {Object=} callbackObjects the object in which the callback function is invoked on.  This is optional.  
 *        You can specify the callback object for each callbacks using the "success" and "error" properties.
 * @export
 */
oj.CollectionDataGridDataSource.prototype.move = function(moveKey, atKey, position, callbacks, callbackObjects)
{
    var indexPromise;
    this.collection.get(moveKey, {deferred: true}).then(
        function(moveModel) {
            if (atKey == null)
            {
                this.collection.remove(moveModel);
                this.collection.add(moveModel);
                if (callbacks != null && callbacks['success'] != null)
                {
                    callbacks['success'].call(callbackObjects['success']);
                }                       
            }
            else
            {
                if (moveKey === atKey)
                {
                    indexPromise = this.collection.indexOf(atKey, {deferred: true});
                    this.collection.remove(moveModel);
                }
                else
                {
                    this.collection.remove(moveModel);
                    indexPromise = this.collection.indexOf(atKey, {deferred: true});
                }

                indexPromise.then(function(newIndex) {
                    this.collection.add(moveModel, {at: newIndex, force:true});
                    if (callbacks != null && callbacks['success'] != null)
                    {
                        callbacks['success'].call(callbackObjects['success']);
                    }                    
                }.bind(this));
            }
        }.bind(this));
};

//////////////////////////////////// Event listeners /////////////////////////////////////
/**
 * Returns an Object for an event 
 * @param {string} operation the operation done on the model
 * @param {Object|null} rowKey the key for the row axis
 * @param {Object|null} columnKey the key for the column axis
 * @param {number=} rowIndex the index for the row axis
 * @param {number=} columnIndex the index for the column axis
 * @return {Object} an object containing the the source, operation, and keys of the event
 * @private
 */
oj.CollectionDataGridDataSource.prototype._getModelEvent = function(operation, rowKey, columnKey, rowIndex, columnIndex)
{
    var event = {};
    event['source'] = this;
    event['operation'] = operation;
    event['keys'] = {'row': rowKey, 'column': columnKey};
    event['indexes'] = {'row': rowIndex, 'column': columnIndex};
    return event;
};

/**
 * Handle a model add to the collection
 * @param {Object} model The model being added to the collection 
 * @param {Object} collection The coleection the model was added to 
 * @param {Object} args additional params passed by the event
 * @private
 */
oj.CollectionDataGridDataSource.prototype._handleModelAdded = function(model, collection, args)
{
    var event, rowKey;
    this.collection.indexOf(model, {deferred: true}).then(function(index)
    {
        rowKey = oj.CollectionDataGridUtils._getModelKey(model);
        event = this._getModelEvent('insert', rowKey, null, index, -1);
        this.handleEvent("change", event);
    }.bind(this));    
};

/**
 * Handle a model delete from the collection
 * @param {Object} model The model being deleted from the collection 
 * @param {Object} collection The coleection the model was added to 
 * @param {Object} args additional params passed by the event
 * @private
 */
oj.CollectionDataGridDataSource.prototype._handleModelDeleted = function(model, collection, args)
{
    var event, rowKey;
    rowKey = oj.CollectionDataGridUtils._getModelKey(model);        
    event = this._getModelEvent('delete', rowKey, null, args['index'], -1);
    this.handleEvent("change", event);
       
};

/**
 * Handle a model change in the collection
 * @param {Object} model The model being changed in the collection  
 * @param {Object} collection The coleection the model was added to 
 * @param {Object} args additional params passed by the event
 * @private
 */
oj.CollectionDataGridDataSource.prototype._handleModelChanged = function(model, collection, args)
{
    var event, rowKey;
	//pass the indexes into the grid on model change
    this.collection.indexOf(model, {deferred: true}).then(function(index) 
    {
        rowKey = oj.CollectionDataGridUtils._getModelKey(model);
        event = this._getModelEvent('update', rowKey, null, index, -1);         
        this.handleEvent("change", event);    
    }.bind(this)); 
};

/**
 * Handle a collection refresh, by firing change event with operation refresh 
 * @private
 */
oj.CollectionDataGridDataSource.prototype._handleCollectionRefresh = function()
{
    var event;
    this.data = null;    
    event = this._getModelEvent('refresh', null, null);
    this.handleEvent("change", event);   
};

/**
 * Handle a collection reset, by firing change event with operation reset 
 * @private
 */
oj.CollectionDataGridDataSource.prototype._handleCollectionReset = function()
{
    var event;
    this.data = null;
    event = this._getModelEvent('reset', null, null);
    this.handleEvent("change", event);
};

/**
 * Get the length of the collection. -1 if an initial fetch has not been
 * done yet. Default to the size of the collection. If pageSize is set then
 * limit it.
 * @returns {number} length of the collection
 * @private
 */
oj.CollectionDataGridDataSource.prototype._size = function() 
{
    return this.collection.size();
};

/**
 * Return the total size of data available, including server side if not local.
 * @returns {number} total size of data
 * @private
 */
oj.CollectionDataGridDataSource.prototype._totalSize = function() 
{ 
    return this.collection['totalResults'] === undefined ? -1:this.collection['totalResults'];
};

//////////////////////////////////// Property Getters  /////////////////////////////////////    
/**
 * Gets the collection property for testing
 * @export
 * @ignore
 */
oj.CollectionDataGridDataSource.prototype.getCollection = function()
{
    return this.collection;
};    

/**
 * Gets the columns property for testing
 * @export
 * @ignore
 */
oj.CollectionDataGridDataSource.prototype.getColumns = function()
{
    return this.columns;
};    

/**
 * Gets the rowHeader property for testing
 * @export
 * @ignore
 */
oj.CollectionDataGridDataSource.prototype.getRowHeader = function()
{
    return this.rowHeader;
};

/**
 * Gets the data property for testing
 * @export
 * @ignore
 */
oj.CollectionDataGridDataSource.prototype.getData = function()
{
    return this.data;
};