@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,327 lines (1,144 loc) • 89.9 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides the OData model implementation of a tree binding
sap.ui.define([
"sap/base/assert",
"sap/base/Log",
"sap/base/util/includes",
"sap/base/util/isEmptyObject",
'sap/ui/model/ChangeReason',
'sap/ui/model/Context',
'sap/ui/model/Filter',
'sap/ui/model/FilterProcessor',
'sap/ui/model/FilterType',
'sap/ui/model/Sorter',
'sap/ui/model/SorterProcessor',
'sap/ui/model/TreeBinding',
'sap/ui/model/TreeBindingUtils',
'sap/ui/model/odata/CountMode',
'sap/ui/model/odata/ODataUtils',
'sap/ui/model/odata/OperationMode',
"sap/ui/thirdparty/jquery"
], function(assert, Log, includes, isEmptyObject, ChangeReason, Context, Filter, FilterProcessor,
FilterType, Sorter, SorterProcessor, TreeBinding, TreeBindingUtils, CountMode, ODataUtils,
OperationMode, jQuery) {
"use strict";
/**
* Do <strong>NOT</strong> call this private constructor, but rather use
* {@link sap.ui.model.odata.v2.ODataModel#bindTree} instead!
*
* @param {sap.ui.model.odata.v2.ODataModel} oModel
* The OData V2 model
* @param {string} sPath
* The binding path, either absolute or relative to a given <code>oContext</code>
* @param {sap.ui.model.Context} [oContext]
* The parent context which is required as base for a relative path
* @param {sap.ui.model.Filter | sap.ui.model.Filter[]} [vFilters]
* The application filters to be used initially
* @param {object} [mParameters]
* Map of binding parameters
* @param {object} [mParameters.treeAnnotationProperties]
* The mapping between data properties and the hierarchy used to visualize the tree, if not
* provided by the service's metadata
* @param {string} [mParameters.treeAnnotationProperties.hierarchyLevelFor]
* The property name in the same type holding the hierarchy level information
* @param {string} [mParameters.treeAnnotationProperties.hierarchyNodeFor]
* The property name in the same type holding the hierarchy node id
* @param {string} [mParameters.treeAnnotationProperties.hierarchyParentNodeFor]
* The property name in the same type holding the parent node id
* @param {string} [mParameters.treeAnnotationProperties.hierarchyDrillStateFor]
* The property name in the same type holding the drill state for the node
* @param {string} [mParameters.treeAnnotationProperties.hierarchyNodeDescendantCountFor]
* The property name in the same type holding the descendant count for the node
* @param {number} [mParameters.numberOfExpandedLevels=0]
* The number of levels that are auto-expanded initially
* @param {number} [mParameters.rootLevel=0]
* The level of the topmost tree nodes
* @param {string} [mParameters.groupId]
* The group id to be used for requests originating from this binding
* @param {sap.ui.model.odata.OperationMode} [mParameters.operationMode]
* The operation mode for this binding
* @param {number} [mParameters.threshold]
* The threshold used if the operation mode is set to
* {@link sap.ui.model.odata.OperationMode.Auto OperationMode.Auto}
* @param {boolean} [mParameters.useServersideApplicationFilters]
* Whether <code>$filter</code> statements should be used for the <code>$count</code> /
* <code>$inlinecount</code> requests and for the data request if the operation mode is
* {@link sap.ui.model.odata.OperationMode.Auto OperationMode.Auto}
* @param {any} [mParameters.treeState]
* A tree state handle
* @param {sap.ui.model.odata.CountMode} [mParameters.countMode]
* The count mode of this binding
* @param {boolean} [mParameters.usePreliminaryContext]
* Whether a preliminary context is used
* @param {string} [mParameters.batchGroupId]
* <b>Deprecated</b>, use <code>groupId</code> instead
* @param {object} [mParameters.navigation]
* <b>Deprecated since 1.44:</b> A map describing the navigation properties between entity
* sets, which is used for constructing and paging the tree
* @param {sap.ui.model.Sorter | sap.ui.model.Sorter[]} [vSorters]
* The dynamic sorters to be used initially
*
* @alias sap.ui.model.odata.v2.ODataTreeBinding
* @author SAP SE
* @class Tree binding implementation for the {@link sap.ui.model.odata.v2.ODataModel}. Use
* {@link sap.ui.model.odata.v2.ODataModel#bindTree} for creating an instance.
* @extends sap.ui.model.TreeBinding
* @hideconstructor
* @public
* @version 1.87.1
*/
var ODataTreeBinding = TreeBinding.extend("sap.ui.model.odata.v2.ODataTreeBinding", /** @lends sap.ui.model.odata.v2.ODataTreeBinding.prototype */ {
constructor : function (oModel, sPath, oContext, vFilters, mParameters, vSorters) {
TreeBinding.apply(this, arguments);
//make sure we have at least an empty parameter object
this.mParameters = this.mParameters || mParameters || {};
this.sGroupId;
this.sRefreshGroupId;
this.oFinalLengths = {};
this.oLengths = {};
this.oKeys = {};
this.bNeedsUpdate = false;
this._bRootMissing = false;
this.bSkipDataEvents = false;
if (vSorters instanceof Sorter) {
vSorters = [vSorters];
}
this.aSorters = vSorters || [];
this.sFilterParams = "";
this.mNormalizeCache = {};
// The ODataTreeBinding expects there to be only an array in this.aApplicationFilters later on.
// Wrap the given application filters inside an array if necessary
if (vFilters instanceof Filter) {
vFilters = [vFilters];
}
this.aApplicationFilters = vFilters;
// check filter integrity
this.oModel.checkFilterOperation(this.aApplicationFilters);
// a queue containing all parallel running requests
// a request is identified by (node id, startindex, length)
this.mRequestHandles = {};
this.oRootContext = null;
this.iNumberOfExpandedLevels = (mParameters && mParameters.numberOfExpandedLevels) || 0;
this.iRootLevel = (mParameters && mParameters.rootLevel) || 0;
this.sCountMode = (mParameters && mParameters.countMode) || this.oModel.sDefaultCountMode;
if (this.sCountMode == CountMode.None) {
Log.fatal("To use an ODataTreeBinding at least one CountMode must be supported by the service!");
}
if (mParameters) {
this.sGroupId = mParameters.groupId || mParameters.batchGroupId;
}
this.bInitial = true;
this._mLoadedSections = {};
this._iPageSize = 0;
// external operation mode
this.sOperationMode = (mParameters && mParameters.operationMode) || this.oModel.sDefaultOperationMode;
if (this.sOperationMode === OperationMode.Default) {
this.sOperationMode = OperationMode.Server;
}
// internal operation mode switch, default is the same as "OperationMode.Server"
this.bClientOperation = false;
// the internal operation mode might change, the external operation mode (this.sOperationMode) will always be the original value
switch (this.sOperationMode) {
case OperationMode.Server: this.bClientOperation = false; break;
case OperationMode.Client: this.bClientOperation = true; break;
case OperationMode.Auto: this.bClientOperation = false; break; //initially start the same as the server mode
}
// the threshold for the OperationMode.Auto
this.iThreshold = (mParameters && mParameters.threshold) || 0;
// flag to check if the threshold was rejected after a count was issued
this.bThresholdRejected = false;
// the total collection count is the number of entries available in the backend (starting at the given rootLevel)
this.iTotalCollectionCount = null;
// a flag to decide if the OperationMode.Auto should "useServersideApplicationFilters", by default the filters are omitted.
this.bUseServersideApplicationFilters = (mParameters && mParameters.useServersideApplicationFilters) || false;
this.bUsePreliminaryContext = this.mParameters.usePreliminaryContext
|| oModel.bPreliminaryContext;
this.oAllKeys = null;
this.oAllLengths = null;
this.oAllFinalLengths = null;
}
});
/**
* Drill-States for Hierarchy-Nodes
*
* From the spec:
* A property holding the drill state of a hierarchy node includes this attribute.
* The drill state is indicated by one of the following values: collapsed, expanded, leaf.
* The value of this attribute is always the name of another property in the same type.
* It points to the related property holding the hierarchy node ID.
*/
ODataTreeBinding.DRILLSTATES = {
Collapsed: "collapsed",
Expanded: "expanded",
Leaf: "leaf"
};
/**
* Builds a node filter string.
* mParams.id holds the ID value for filtering on the hierarchy node.
*/
ODataTreeBinding.prototype._getNodeFilterParams = function (mParams) {
var sPropName = mParams.isRoot ? this.oTreeProperties["hierarchy-node-for"] : this.oTreeProperties["hierarchy-parent-node-for"];
var oEntityType = this._getEntityType();
return ODataUtils._createFilterParams(new Filter(sPropName, "EQ", mParams.id), this.oModel.oMetadata, oEntityType);
};
/**
* Builds the Level-Filter string
*/
ODataTreeBinding.prototype._getLevelFilterParams = function (sOperator, iLevel) {
var oEntityType = this._getEntityType();
return ODataUtils._createFilterParams(new Filter(this.oTreeProperties["hierarchy-level-for"], sOperator, iLevel), this.oModel.oMetadata, oEntityType);
};
/**
* Retrieves the root node given through sNodeId
* @param {string} sNodeId the ID od the root node which should be loaded (e.g. when bound to a single entity)
* @param {string} sRequestKey a key string used to store/clean-up request handles
* @private
*/
ODataTreeBinding.prototype._loadSingleRootNodeByNavigationProperties = function (sNodeId, sRequestKey) {
var that = this,
sGroupId;
if (this.mRequestHandles[sRequestKey]) {
this.mRequestHandles[sRequestKey].abort();
}
sGroupId = this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId;
var sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext());
if (sAbsolutePath) {
this.mRequestHandles[sRequestKey] = this.oModel.read(sAbsolutePath, {
groupId: sGroupId,
success: function (oData) {
var sNavPath = that._getNavPath(that.getPath());
if (oData) {
// we expect only one root node
var oEntry = oData;
var sKey = that.oModel._getKey(oEntry);
var oNewContext = that.oModel.getContext('/' + sKey);
that.oRootContext = oNewContext;
that._processODataObject(oNewContext.getObject(), sNodeId, sNavPath);
} else {
that._bRootMissing = true;
}
that.bNeedsUpdate = true;
delete that.mRequestHandles[sRequestKey];
that.oModel.callAfterUpdate(function() {
that.fireDataReceived({data: oData});
});
},
error: function (oError) {
//Only perform error handling if the request was not aborted intentionally
if (oError && oError.statusCode != 0 && oError.statusText != "abort") {
that.bNeedsUpdate = true;
that._bRootMissing = true;
delete that.mRequestHandles[sRequestKey];
that.fireDataReceived();
}
}
});
}
};
/**
* Returns root contexts for the tree. You can specify the start index and the length for paging requests.
* This function is not available when the annotation "hierarchy-node-descendant-count-for" is exposed on the service.
*
* @param {int} [iStartIndex=0] the start index of the requested contexts
* @param {int} [iLength=v2.ODataModel.sizeLimit] the requested amount of contexts. If none given, the default value is the size limit of the underlying
* sap.ui.model.odata.v2.ODataModel instance.
* @param {int} [iThreshold=0] the number of entities which should be retrieved in addition to the given length.
* A higher threshold reduces the number of backend requests, yet these request blow up in size, since more data is loaded.
* @return {sap.ui.model.Context[]} an array containing the contexts for the entities returned by the backend, might be fewer than requested
* if the backend does not have enough data.
* @public
*/
ODataTreeBinding.prototype.getRootContexts = function(iStartIndex, iLength, iThreshold) {
var sNodeId = null,
mRequestParameters = {
numberOfExpandedLevels: this.iNumberOfExpandedLevels
},
aRootContexts = [];
if (this.isInitial()) {
return aRootContexts;
}
// make sure the input parameters are not undefined
iStartIndex = iStartIndex || 0;
iLength = iLength || this.oModel.sizeLimit;
iThreshold = iThreshold || 0;
// node ID for the root context(s) ~> null
// startindex/length may differ due to paging
// same node id + different paging sections are treated as different requests and will not abort each other
var sRequestKey = "" + sNodeId + "-" + iStartIndex + "-" + this._iPageSize + "-" + iThreshold;
if (this.bHasTreeAnnotations) {
this.bDisplayRootNode = true;
// load root level, node id is "null" in this case
aRootContexts = this._getContextsForNodeId(null, iStartIndex, iLength, iThreshold);
} else {
sNodeId = this.oModel.resolve(this.getPath(), this.getContext());
var bIsList = this.oModel.isList(this.sPath, this.getContext());
if (bIsList) {
this.bDisplayRootNode = true;
}
if (this.bDisplayRootNode && !bIsList) {
if (this.oRootContext) {
return [this.oRootContext];
} else if (this._bRootMissing) {
// the backend may not return anything for the given root node, so in this case our root node is missing
return [];
} else {
this._loadSingleRootNodeByNavigationProperties(sNodeId, sRequestKey);
}
} else {
mRequestParameters.navPath = this._getNavPath(this.getPath());
//append nav path if binding path is not a collection and the root node should not be displayed
if (!this.bDisplayRootNode) {
sNodeId += "/" + mRequestParameters.navPath;
}
aRootContexts = this._getContextsForNodeId(sNodeId, iStartIndex, iLength, iThreshold, mRequestParameters);
}
}
return aRootContexts;
};
/**
* Returns the contexts of the child nodes for the given context. This function is not available when the annotation "hierarchy-node-descendant-count-for"
* is exposed on the service.
*
* @param {sap.ui.model.Context} oContext the context for which the child nodes should be retrieved
* @param {int} iStartIndex the start index of the requested contexts
* @param {int} iLength the requested amount of contexts
* @param {int} iThreshold
* @return {sap.ui.model.Context[]} the contexts array
* @public
*/
ODataTreeBinding.prototype.getNodeContexts = function(oContext, iStartIndex, iLength, iThreshold) {
var sNodeId,
mRequestParameters = {};
if (this.isInitial()) {
return [];
}
if (this.bHasTreeAnnotations) {
// previously only the Hierarchy-ID-property from the data was used as key but not the actual OData-Key
// now the actual key of the odata entry is used
sNodeId = this.oModel.getKey(oContext);
mRequestParameters.level = parseInt(oContext.getProperty(this.oTreeProperties["hierarchy-level-for"])) + 1;
} else {
var sNavPath = this._getNavPath(oContext.getPath());
//If no nav path was found no nav property is defined and we cannot find any more data
if (!sNavPath) {
return [];
}
sNodeId = this.oModel.resolve(sNavPath, oContext);
mRequestParameters.navPath = this.oNavigationPaths[sNavPath];
}
return this._getContextsForNodeId(sNodeId, iStartIndex, iLength, iThreshold, mRequestParameters);
};
/**
* Returns if the node has child nodes.
* If the ODataTreeBinding is running with hierarchy annotations, a context with the property values "expanded" or "collapsed"
* for the drilldown state property, returns true. Entities with drilldown state "leaf" return false.
*
* This function is not available when the annotation "hierarchy-node-descendant-count-for" is exposed on the service.
*
* @param {sap.ui.model.Context} oContext the context element of the node
* @return {boolean} true if node has children
*
* @public
*/
ODataTreeBinding.prototype.hasChildren = function(oContext) {
if (this.bHasTreeAnnotations) {
if (!oContext) {
return false;
}
var sDrilldownState = oContext.getProperty(this.oTreeProperties["hierarchy-drill-state-for"]);
var sNodeKey = this.oModel.getKey(oContext);
//var sHierarchyNode = oContext.getProperty(this.oTreeProperties["hierarchy-node-for"]);
var iLength = this.oLengths[sNodeKey];
// if the server returned no children for a node (even though it has a DrilldownState of "expanded"),
// the length for this node is set to 0 and finalized -> no children available
if (iLength === 0 && this.oFinalLengths[sNodeKey]) {
return false;
}
// leaves do not have childre, only "expanded" and "collapsed" nodes
// Beware: the drilldownstate may be undefined/empty string,
// in case the entity (oContext) has no value for the drilldown state property
if (sDrilldownState === "expanded" || sDrilldownState === "collapsed") {
return true;
} else if (sDrilldownState === "leaf"){
return false;
} else {
Log.warning("The entity '" + oContext.getPath() + "' has not specified Drilldown State property value.");
//fault tolerance for empty property values (we optimistically say that those nodes can be expanded/collapsed)
if (sDrilldownState === undefined || sDrilldownState === "") {
return true;
}
return false;
}
} else {
if (!oContext) {
return this.oLengths[this.getPath()] > 0;
}
var iLength = this.oLengths[oContext.getPath() + "/" + this._getNavPath(oContext.getPath())];
//only return false if we definitely know that the length is 0, otherwise, we have either a known length or none at all (undefined)
return iLength !== 0;
}
};
/**
* Returns the number of child nodes. This function is not available when the annotation "hierarchy-node-descendant-count-for"
* is exposed on the service.
*
* @param {Object} oContext the context element of the node
* @return {int} the number of children
*
* @public
*/
ODataTreeBinding.prototype.getChildCount = function(oContext) {
if (this.bHasTreeAnnotations) {
var vHierarchyNode;
// only the root node should have no context
// the child count is either stored via the rootNodeId or (if only the rootLevel is given) as "null", because we do not know the root id
if (!oContext) {
vHierarchyNode = null;
} else {
vHierarchyNode = this.oModel.getKey(oContext);
}
return this.oLengths[vHierarchyNode];
} else {
if (!oContext) {
// if no context was given, we retrieve the top-level child count:
// 1. in case the binding path is a collection we need use the binding path as a key in the length map
// 2. in case the binding path is a single entity, we need to add the navigation property from the "$expand" query option
if (!this.bDisplayRootNode) {
return this.oLengths[this.getPath() + "/" + this._getNavPath(this.getPath())];
} else {
return this.oLengths[this.getPath()];
}
}
return this.oLengths[oContext.getPath() + "/" + this._getNavPath(oContext.getPath())];
}
};
/**
* Gets or loads all contexts for a specified node id (dependent on mode)
*
* @param {string} sNodeId the value of the hierarchy node property on which a parent node filter will be performed
* @param {int} iStartIndex start index of the page
* @param {int} iLength length of the page
* @param {int} iThreshold additionally loaded entities
* @param {object} mParameters additional request parameters
*
* @return {sap.ui.model.Context[]} Array of contexts
*
* @private
*/
ODataTreeBinding.prototype._getContextsForNodeId = function(sNodeId, iStartIndex, iLength, iThreshold, mRequestParameters) {
var aContexts = [],
sKey;
// OperationMode.Auto: handle synchronized count to check what the actual internal operation mode should be
// If the $count or $inlinecount is used, is determined by the respective
if (this.sOperationMode == OperationMode.Auto) {
// as long as we do not have a collection count, we return an empty array
if (this.iTotalCollectionCount == null) {
if (!this.bCollectionCountRequested) {
this._getCountForCollection();
this.bCollectionCountRequested = true;
}
return [];
}
}
// Set default values if startindex, threshold or length are not defined
iStartIndex = iStartIndex || 0;
iLength = iLength || this.oModel.iSizeLimit;
iThreshold = iThreshold || 0;
// re-set the threshold in OperationMode.Auto
// between binding-treshold and the threshold given as an argument, the bigger one will be taken
if (this.sOperationMode == OperationMode.Auto) {
if (this.iThreshold >= 0) {
iThreshold = Math.max(this.iThreshold, iThreshold);
}
}
if (!this._mLoadedSections[sNodeId]) {
this._mLoadedSections[sNodeId] = [];
}
// make sure we only request the maximum length available (length is known and final)
if (this.oFinalLengths[sNodeId] && this.oLengths[sNodeId] < iStartIndex + iLength) {
iLength = Math.max(this.oLengths[sNodeId] - iStartIndex, 0);
}
var that = this;
// check whether a start index was already requested
var fnFindInLoadedSections = function(iStartIndex) {
// check in the sections which where loaded
for (var i = 0; i < that._mLoadedSections[sNodeId].length; i++) {
var oSection = that._mLoadedSections[sNodeId][i];
// try to find i in the loaded sections. If i is within one of the sections it needs not to be loaded again
if (iStartIndex >= oSection.startIndex && iStartIndex < oSection.startIndex + oSection.length) {
return true;
}
}
// check requested sections where we still wait for an answer
};
var aMissingSections = [];
// Loop through known data and check whether we already have all rows loaded
// make sure to also check that the entities before the requested start index can be served
var i = Math.max((iStartIndex - iThreshold - this._iPageSize), 0);
if (this.oKeys[sNodeId]) {
// restrict loop to the maximum available length if we have a $(inline)count
// this will make sure we do not find "missing" sections at the end of the known datablock, if it is outside the $(inline)count
var iMaxIndexToCheck = iStartIndex + iLength + (iThreshold);
if (this.oLengths[sNodeId]) {
iMaxIndexToCheck = Math.min(iMaxIndexToCheck, this.oLengths[sNodeId]);
}
for (i; i < iMaxIndexToCheck; i++) {
sKey = this.oKeys[sNodeId][i];
if (!sKey) {
//only collect missing sections if we are running in the internal operationMode "Server" -> bClientOperation = false
if (!this.bClientOperation && !fnFindInLoadedSections(i)) {
aMissingSections = TreeBindingUtils.mergeSections(aMissingSections, {startIndex: i, length: 1});
}
}
// collect requested contexts if loaded
if (i >= iStartIndex && i < iStartIndex + iLength) {
if (sKey) {
aContexts.push(this.oModel.getContext('/' + sKey));
} else {
aContexts.push(undefined);
}
}
}
// check whether the missing section already spans the complete page. If this is the case, we don't need to request an additional page
var iBegin = Math.max((iStartIndex - iThreshold - this._iPageSize), 0);
var iEnd = iStartIndex + iLength + (iThreshold);
var bExpandThreshold = aMissingSections[0] && aMissingSections[0].startIndex === iBegin && aMissingSections[0].startIndex + aMissingSections[0].length === iEnd;
if (aMissingSections.length > 0 && !bExpandThreshold) {
//first missing section will be prepended with additional threshold ("negative")
i = Math.max((aMissingSections[0].startIndex - iThreshold - this._iPageSize), 0);
var iFirstStartIndex = aMissingSections[0].startIndex;
for (i; i < iFirstStartIndex; i++) {
var sKey = this.oKeys[sNodeId][i];
if (!sKey) {
if (!fnFindInLoadedSections(i)) {
aMissingSections = TreeBindingUtils.mergeSections(aMissingSections, {startIndex: i, length: 1});
}
}
}
//last missing section will be appended with additional threshold ("positive")
i = aMissingSections[aMissingSections.length - 1].startIndex + aMissingSections[aMissingSections.length - 1].length;
var iEndIndex = i + iThreshold + this._iPageSize;
// if we already have a count -> clamp the end index
if (this.oLengths[sNodeId]) {
iEndIndex = Math.min(iEndIndex, this.oLengths[sNodeId]);
}
for (i; i < iEndIndex; i++) {
var sKey = this.oKeys[sNodeId][i];
if (!sKey) {
if (!fnFindInLoadedSections(i)) {
aMissingSections = TreeBindingUtils.mergeSections(aMissingSections, {startIndex: i, length: 1});
}
}
}
}
} else {
// for initial loading of a node use this shortcut.
if (!fnFindInLoadedSections(iStartIndex)) {
// "i" is our shifted forward startIndex for the "negative" thresholding
// in this case i is always smaller than iStartIndex, but minimum is 0
var iLengthShift = iStartIndex - i;
aMissingSections = TreeBindingUtils.mergeSections(aMissingSections, {startIndex: i, length: iLength + iLengthShift + iThreshold});
}
}
// check if metadata are already available
if (this.oModel.getServiceMetadata()) {
// If rows are missing send a request
if (aMissingSections.length > 0) {
var aParams = [];
var sFilterParams = "";
if (this.bHasTreeAnnotations) {
if (this.sOperationMode == "Server" || this.bUseServersideApplicationFilters) {
sFilterParams = this.getFilterParams();
//sFilterParams = sFilterParams ? "%20and%20" + sFilterParams : "";
}
if (sNodeId) {
sFilterParams = sFilterParams ? "%20and%20" + sFilterParams : "";
//retrieve the correct context for the sNodeId (it's an OData-Key) and resolve the correct hierarchy node property as a filter value
var oNodeContext = this.oModel.getContext("/" + sNodeId);
var sNodeIdForFilter = oNodeContext.getProperty(this.oTreeProperties["hierarchy-node-for"]);
//construct node filter parameter
var sNodeFilterParameter = this._getNodeFilterParams({id: sNodeIdForFilter});
aParams.push("$filter=" + sNodeFilterParameter + sFilterParams);
} else if (sNodeId == null) {
// no root node id is given: sNodeId === null
// in this case we use the root level
// in case the binding runs in OperationMode Server -> the level filter is EQ by default,
// for the Client OperationMode GT is used to fetch all nodes below the given level
// The only exception here is the rootLevel 0:
// if the root Level is 0, we do not send any level filters, since by specification the top level nodes are on level 0
// this is for compatibility reasons with different backend-systems, which do not support GE operators on the level
var sLevelFilter = "";
if (!this.bClientOperation || this.iRootLevel > 0) {
var sLevelFilterOperator = this.bClientOperation ? "GE" : "EQ";
sLevelFilter = this._getLevelFilterParams(sLevelFilterOperator, this.iRootLevel);
}
//only build filter statement if necessary
if (sLevelFilter || sFilterParams) {
//if we have a level filter AND an application filter, we need to add an escaped "AND" to between
if (sFilterParams && sLevelFilter) {
sFilterParams = "%20and%20" + sFilterParams;
}
aParams.push("$filter=" + sLevelFilter + sFilterParams);
}
}
} else {
// append application filters for navigation property case
sFilterParams = this.getFilterParams();
if (sFilterParams) {
aParams.push("$filter=" + sFilterParams);
}
}
if (this.sCustomParams) {
aParams.push(this.sCustomParams);
}
if (!this.bClientOperation) {
// request the missing sections and manage the loaded sections map
for (i = 0; i < aMissingSections.length; i++) {
var oRequestedSection = aMissingSections[i];
this._mLoadedSections[sNodeId] = TreeBindingUtils.mergeSections(this._mLoadedSections[sNodeId], {startIndex: oRequestedSection.startIndex, length: oRequestedSection.length});
this._loadSubNodes(sNodeId, oRequestedSection.startIndex, oRequestedSection.length, 0, aParams, mRequestParameters, oRequestedSection);
}
} else {
// OperationMode is set to "Client" AND we have something missing (should only happen once, at the very first loading request)
// of course also make sure no request is running already
if (!this.oAllKeys && !this.mRequestHandles[ODataTreeBinding.REQUEST_KEY_CLIENT]) {
this._loadCompleteTreeWithAnnotations(aParams);
}
}
}
}
return aContexts;
};
/**
* Simple request to count how many nodes are available in the collection, starting at the given rootLevel.
* Depending on the countMode of the binding, either a $count or a $inlinecount is sent.
*/
ODataTreeBinding.prototype._getCountForCollection = function () {
if (!this.bHasTreeAnnotations || this.sOperationMode != OperationMode.Auto) {
Log.error("The Count for the collection can only be retrieved with Hierarchy Annotations and in OperationMode.Auto.");
return;
}
// create a request object for the data request
var aParams = [];
function _handleSuccess(oData) {
// $inlinecount is in oData.__count, the $count is just oData
var iCount = oData.__count ? parseInt(oData.__count) : parseInt(oData);
this.iTotalCollectionCount = iCount;
// in the OpertionMode.Auto, we check if the count is LE than the given threshold and set the client operation flag accordingly
if (this.sOperationMode == OperationMode.Auto) {
if (this.iTotalCollectionCount <= this.iThreshold) {
this.bClientOperation = true;
this.bThresholdRejected = false;
} else {
this.bClientOperation = false;
this.bThresholdRejected = true;
}
this._fireChange({reason: ChangeReason.Change});
}
}
function _handleError(oError) {
// Only perform error handling if the request was not aborted intentionally
if (oError && oError.statusCode === 0 && oError.statusText === "abort") {
return;
}
var sErrorMsg = "Request for $count failed: " + oError.message;
if (oError.response){
sErrorMsg += ", " + oError.response.statusCode + ", " + oError.response.statusText + ", " + oError.response.body;
}
Log.warning(sErrorMsg);
}
var sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext());
// default filter is on the rootLevel
var sLevelFilter = "";
if (this.iRootLevel > 0) {
sLevelFilter = this._getLevelFilterParams("GE", this.getRootLevel());
}
// if necessary we add all other filters to the count request
var sFilterParams = "";
if (this.bUseServersideApplicationFilters) {
var sFilterParams = this.getFilterParams();
}
//only build filter statement if necessary
if (sLevelFilter || sFilterParams) {
//if we have a level filter AND an application filter, we need to add an escaped "AND" to between
if (sFilterParams && sLevelFilter) {
sFilterParams = "%20and%20" + sFilterParams;
}
aParams.push("$filter=" + sLevelFilter + sFilterParams);
}
// figure out how to request the count
var sCountType = "";
if (this.sCountMode == CountMode.Request || this.sCountMode == CountMode.Both) {
sCountType = "/$count";
} else if (this.sCountMode == CountMode.Inline || this.sCountMode == CountMode.InlineRepeat) {
aParams.push("$top=0");
aParams.push("$inlinecount=allpages");
}
// send the counting request
if (sAbsolutePath) {
this.oModel.read(sAbsolutePath + sCountType, {
urlParameters: aParams,
success: _handleSuccess.bind(this),
error: _handleError.bind(this),
groupId: this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId
});
}
};
/**
* Issues a $count request for the given node-id/odata-key.
* Only used when running in CountMode.Request. Inlinecounts are appended directly when issuing a loading request.
* @private
*/
ODataTreeBinding.prototype._getCountForNodeId = function(sNodeId, iStartIndex, iLength, iThreshold, mParameters) {
var that = this,
sGroupId;
// create a request object for the data request
var aParams = [];
function _handleSuccess(oData) {
that.oFinalLengths[sNodeId] = true;
that.oLengths[sNodeId] = parseInt(oData);
}
function _handleError(oError) {
//Only perform error handling if the request was not aborted intentionally
if (oError && oError.statusCode === 0 && oError.statusText === "abort") {
return;
}
var sErrorMsg = "Request for $count failed: " + oError.message;
if (oError.response){
sErrorMsg += ", " + oError.response.statusCode + ", " + oError.response.statusText + ", " + oError.response.body;
}
Log.warning(sErrorMsg);
}
var sAbsolutePath;
var sFilterParams = this.getFilterParams() || "";
var sNodeFilter = "";
if (this.bHasTreeAnnotations) {
//resolve OData-Key to hierarchy node property value for filtering
var oNodeContext = this.oModel.getContext("/" + sNodeId);
var sHierarchyNodeId = oNodeContext.getProperty(this.oTreeProperties["hierarchy-node-for"]);
sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext());
// only filter for the parent node if the given node is not the root (null)
// if root and we $count the collection
if (sNodeId != null) {
sNodeFilter = this._getNodeFilterParams({id: sHierarchyNodeId});
} else {
sNodeFilter = this._getLevelFilterParams("EQ", this.getRootLevel());
}
} else {
sAbsolutePath = sNodeId;
}
if (sNodeFilter || sFilterParams) {
var sAnd = "";
if (sNodeFilter && sFilterParams) {
sAnd = "%20and%20";
}
sFilterParams = "$filter=" + sFilterParams + sAnd + sNodeFilter;
aParams.push(sFilterParams);
}
// Only send request, if path is defined
if (sAbsolutePath) {
sGroupId = this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId;
this.oModel.read(sAbsolutePath + "/$count", {
urlParameters: aParams,
success: _handleSuccess,
error: _handleError,
sorters: this.aSorters,
groupId: sGroupId
});
}
};
/**
* Retrieves parent ids from a given data set
*
* @param {Array} aData Lookup array to search for parent ids
* @param {Boolean} bExcludeRootNodes Can be set to exclude root node elements
* @returns {Array} Array of all parent ids
*
* @private
*/
ODataTreeBinding.prototype._getParentMap = function(aData) {
var mParentKeys = {};
for (var i = 0; i < aData.length; i++) {
var sID = aData[i][this.oTreeProperties["hierarchy-node-for"]];
if (mParentKeys[sID]) {
Log.warning("ODataTreeBinding: Duplicate key: " + sID + "!");
}
mParentKeys[sID] = this.oModel._getKey(aData[i]);
}
return mParentKeys;
};
/**
* Creates key map for given data
*
* @param {Array} aData data which should be preprocessed
* @returns {Object<string,string[]>} Map of parent and child keys
*
* @private
*/
ODataTreeBinding.prototype._createKeyMap = function(aData, bSkipFirstNode) {
if (aData && aData.length > 0) {
var mParentsKeys = this._getParentMap(aData), mKeys = {};
for (var i = bSkipFirstNode ? 1 : 0; i < aData.length; i++) {
var sParentNodeID = aData[i][this.oTreeProperties["hierarchy-parent-node-for"]],
sParentKey = mParentsKeys[sParentNodeID];
if (parseInt(aData[i][this.oTreeProperties["hierarchy-level-for"]]) === this.iRootLevel) {
sParentKey = "null";
}
if (!mKeys[sParentKey]) {
mKeys[sParentKey] = [];
}
// add the current entry key to the key map, as a child of its parent node
mKeys[sParentKey].push(this.oModel._getKey(aData[i]));
}
return mKeys;
}
};
/**
* Should import the complete keys hierarchy.
*
* @param {Object<string,string[]>} mKeys Keys to add
*
* @private
*/
ODataTreeBinding.prototype._importCompleteKeysHierarchy = function (mKeys) {
var iChildCount, sKey;
for (sKey in mKeys) {
iChildCount = mKeys[sKey].length || 0;
this.oKeys[sKey] = mKeys[sKey];
// update the length of the parent node
this.oLengths[sKey] = iChildCount;
this.oFinalLengths[sKey] = true;
// keep up with the loaded sections
this._mLoadedSections[sKey] = [ { startIndex: 0, length: iChildCount } ];
}
};
/**
* Update node key in case if it changes
*
* @param {object} oNode
* @param {string} sNewKey
*
* @private
*/
ODataTreeBinding.prototype._updateNodeKey = function (oNode, sNewKey) {
var sOldKey = this.oModel.getKey(oNode.context),
sParentKey, nIndex;
if (parseInt(oNode.context.getProperty(this.oTreeProperties["hierarchy-level-for"])) === this.iRootLevel) {
sParentKey = "null";
} else {
sParentKey = this.oModel.getKey(oNode.parent.context);
}
nIndex = this.oKeys[sParentKey].indexOf(sOldKey);
if (nIndex !== -1) {
this.oKeys[sParentKey][nIndex] = sNewKey;
} else {
this.oKeys[sParentKey].push(sNewKey);
}
};
/**
* Triggers backend requests to load the subtree of a given node
*
* @param {object} oNode Root node of the requested subtree
* @param {string[]} aParams OData URL parameters
* @return {Promise} A promise resolving once the data has been imported
*
* @private
*/
ODataTreeBinding.prototype._loadSubTree = function (oNode, aParams) {
return new Promise(function (resolve, reject) {
var sRequestKey, sGroupId, sAbsolutePath;
// Prevent data from loading if no tree annotation is available
if (!this.bHasTreeAnnotations) {
reject(new Error("_loadSubTree: doesn't support hierarchies without tree annotations"));
return;
}
sRequestKey = "loadSubTree-" + aParams.join("-");
// Skip previous request
if (this.mRequestHandles[sRequestKey]) {
this.mRequestHandles[sRequestKey].abort();
}
var fnSuccess = function (oData) {
// Collecting contexts
// beware: oData.results can be an empty array -> so the length has to be checked
if (oData.results.length > 0) {
var sParentKey = this.oModel.getKey(oData.results[0]);
this._updateNodeKey(oNode, sParentKey);
var mKeys = this._createKeyMap(oData.results);
this._importCompleteKeysHierarchy(mKeys);
}
delete this.mRequestHandles[sRequestKey];
this.bNeedsUpdate = true;
this.oModel.callAfterUpdate(function () {
this.fireDataReceived({ data: oData });
}.bind(this));
resolve(oData);
}.bind(this);
var fnError = function (oError) {
delete this.mRequestHandles[sRequestKey];
//Only perform error handling if the request was not aborted intentionally
if (oError && oError.statusCode === 0 && oError.statusText === "abort") {
return;
}
this.fireDataReceived();
reject(); // Application should retrieve error details via ODataModel events
}.bind(this);
// execute the request and use the metadata if available
if (!this.bSkipDataEvents) {
this.fireDataRequested();
}
this.bSkipDataEvents = false;
sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext());
if (sAbsolutePath) {
sGroupId = this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId;
this.mRequestHandles[sRequestKey] = this.oModel.read(sAbsolutePath, {
urlParameters: aParams,
success: fnSuccess,
error: fnError,
sorters: this.aSorters,
groupId: sGroupId
});
}
}.bind(this));
};
/**
* Triggers backend requests to load the child nodes of the node with the given sNodeId.
*
* @param {string} sNodeId the value of the hierarchy node property on which a parent node filter will be performed
* @param {int} iStartIndex start index of the page
* @param {int} iLength length of the page
* @param {int} iThreshold additionally loaded entities
* @param {array} aParams OData URL parameters, already concatenated with "="
* @param {object} mParameters additional request parameters
* @param {object} mParameters.navPath the navigation path
*
* @return {sap.ui.model.Context[]} Array of contexts
*
* @private
*/
ODataTreeBinding.prototype._loadSubNodes = function(sNodeId, iStartIndex, iLength, iThreshold, aParams, mParameters, oRequestedSection) {
var that = this,
sGroupId,
bInlineCountRequested = false;
// Only append $skip/$top values if we run in OperationMode "Server".
// When the OperationMode is set to "Client", we will fetch the whole collection
if ((iStartIndex || iLength) && !this.bClientOperation) {
aParams.push("$skip=" + iStartIndex + "&$top=" + (iLength + iThreshold));
}
//check if we already have a count
if (!this.oFinalLengths[sNodeId] || this.sCountMode == CountMode.InlineRepeat) {
// issue $inlinecount
if (this.sCountMode == CountMode.Inline || this.sCountMode == CountMode.InlineRepeat || this.sCountMode == CountMode.Both) {
aParams.push("$inlinecount=allpages");
bInlineCountRequested = true;
} else if (this.sCountMode == CountMode.Request) {
//... or $count request
that._getCountForNodeId(sNodeId);
}
}
var sRequestKey = "" + sNodeId + "-" + iStartIndex + "-" + this._iPageSize + "-" + iThreshold;
function fnSuccess(oData) {
if (oData) {
// make sure we have a keys array
that.oKeys[sNodeId] = that.oKeys[sNodeId] || [];
// evaluate the count
if (bInlineCountRequested && oData.__count >= 0) {
that.oLengths[sNodeId] = parseInt(oData.__count);
that.oFinalLengths[sNodeId] = true;
}
}
// Collecting contexts
// beware: oData.results can be an empty array -> so the length has to be checked
if (Array.isArray(oData.results) && oData.results.length > 0) {
// Case 1: Result is an entity set
// Case 1a: Tree Annotations
if (that.bHasTreeAnnotations) {
var mLastNodeIdIndices = {};
for (var i = 0; i < oData.results.length; i++) {
var oEntry = oData.results[i];
if (i == 0) {
mLastNodeIdIndices[sNodeId] = iStartIndex;
} else if (mLastNodeIdIndices[sNodeId] == undefined) {
mLastNodeIdIndices[sNodeId] = 0;
}
that.oKeys[sNodeId][mLastNodeIdIndices[sNodeId]] = that.oModel._getKey(oEntry);
mLastNodeIdIndices[sNodeId]++;
}
} else {
// Case 1b: Navigation Properties
for (var i = 0; i < oData.results.length; i++) {
var oEntry = oData.results[i];
var sKey = that.oModel._getKey(oEntry);
that._processODataObject(oEntry, "/" + sKey, mParameters.navPath);
that.oKeys[sNodeId][i + iStartIndex] = sKey;
}
}
} else if (oData && !Array.isArray(oData.results)){
// Case 2: oData.results is not an array, so oData is a single entity
// this only happens if you bind to a single entity as root element)
that.oKeys[null] = that.oModel._getKey(oData);
if (!that.bHasTreeAnnotations) {
that._processODataObject(oData, sNodeId, mParameters.navPath);
}
}
delete that.mRequestHandles[sRequestKey];
that.bNeedsUpdate = true;
that.oModel.callAfterUpdate(function() {
that.fireDataReceived({data: oData});
});
}
function fnError(oError) {
//Only perform error handling if the request was not aborted intentionally
if (oError && oError.statusCode === 0 && oError.statusText === "abort") {
return;
}
that.fireDataReceived();
delete that.mRequestHandles[sRequestKey];
if (oRequestedSection) {
// remove section from loadedSections so the data can be requested again.
// this might be required when e.g. when the service was not available for a short time
var aLoadedSections = [];
for (var i = 0; i < that._mLoadedSections[sNodeId].length; i++) {
var oCurrentSection = that._mLoadedSections[sNodeId][i];
if (oRequestedSection.startIndex >= oCurrentSection.startIndex && oRequestedSection.startIndex + oRequestedSection.length <= oCurrentSection.startIndex + oCurrentSection.length) {
// remove the section interval and maintain adapted sections. If start index and length are the same, ignore the section
if (oRequestedSection.startIndex !== oCurrentSection.startIndex && oRequestedSection.length !== oCurrentSection.length) {
aLoadedSections = TreeBindingUtils.mergeSections(aLoadedSections, {startIndex: oCurrentSection.startIndex, length: oRequestedSection.startIndex - oCurrentSection.startIndex});
aLoadedSections = TreeBindingUtils.mergeSections(aLoadedSections, {startIndex: oRequestedSection.startIndex + oRequestedSection.length, length: (oCurrentSection.startIndex + oCurrentSection.length) - (oRequestedSection.startIndex + oRequestedSection.length)});
}
} else {
aLoadedSections.push(oCurrentSection);
}
}
that._mLoadedSections[sNodeId] = aLoadedSections;
}
}
// !== because we use "null" as sNodeId in case the user only provided a root level
if (sNodeId !== undefined) {
// execute the request and use the metadata if available
if (!this.bSkipDataEvents) {
this.fireDataRequested();
}
this.bSkipDataEvents = false;
var sAbsolutePath;
if (this.bHasTreeAnnotations) {
sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext());
} else {
sAbsolutePath = sNodeId;
}
if (this.mRequestHandles[sRequestKey]) {
this.mRequestHandles[sRequestKey].abort();
}
if (sAbsolutePath) {
sGroupId = this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId;
this.mRequestHandles[sRequestKey] = this.oModel.read(sAbsolutePath, {
urlParameters: aParams,
success: fnSuccess,
error: fnError,
sorters: this.aSorters,
groupId: sGroupId
});
}
}
};
ODataTreeBinding.REQUEST_KEY_CLIENT = "_OPERATIONMODE_CLIENT_TREE_LOADING";
/**
* Loads the complete collection from the given binding path.
* The tree is then reconstructed from the response entries based on the properties with hierarchy annotations.
* Adds additional URL parameters.
*/
ODataTreeBinding.prototype._loadCompleteTreeWithAnnotations = function (aURLParams) {
var that = this;
var sRequestKey = ODataTreeBinding.REQUEST_KEY_CLIENT;
var fnSuccess = function (oData) {
// all nodes on root level -> save in this.oKeys[null] = [] (?)
if (oData.results && oData.results.length > 0) {
//collect mapping table between parent node id and actual OData-Key
var mParentIds = {};
var oDataObj;
for (var k = 0; k < oData.results.length; k++) {
oDataObj = oData.results[k];
var sDataKey = oDataObj[that.oTreeProperties["hierarchy-node-for"]];
// sanity check: if we have duplicate keys, the data is messed up. Has already happend...
if (mParentIds[sDataKey]) {
Log.warning("ODataTreeBinding - Duplicate data entry for key: " + sDataKey + "!");
}
mParentIds[sDataKey] = that.oModel._getKey(oDataObj);
}
// process data and built tree
for (var i = 0; i < oData.results.length; i++) {
oDataObj = oData.results[i];
var sParentNodeId = oDataObj[that.oTreeProperties["hierarchy-parent-node-for"]];
var sParentKey = mParentIds[sParentNodeId]; //oDataObj[that.oTreeProperties["hierarchy-parent-node-for"]];
// the parentNodeID for root nodes (node level == iRootLevel) is "null"
if (parseInt(oDataObj[that.oTreeProperties["hierarchy-level-for"]]) === that.iRootLevel) {
sParentKey = "null";
}
// make sure the parent node is already present in the key map
that.oKeys[sParentKey] = that.oKeys[sParentKey] || [];
// add the current entry key to the key map, as a child of its parent node
var sKey = that.oModel._getKey(oDataObj);
that.oKeys[sParentKey].push(sKey);
// update the length of the parent node
that.oLengths[sParentKey] = that.oLengths[sParentKey] || 0;
that.oLengths[sParentKey]++;
that.oFinalLengths[sParentKey] = true;
// keep up with the loaded sections
that._mLoadedSections[sParentKey] = that._mLoadedSections[sParentKey] || [];
that._mLoadedSections[sParentKey][0] = that._mLoadedSections[sParentKey][0] || {startIndex: 0, length: 0};
that._mLoadedSections[sParentKey][0].length++;
}
} else {
// no data received -> empty tree
that.oKeys["null"] = [];
that.oLengths["null"] = 0;
that.oFinalLengths["null"] = true;
}
that.oAllKeys = jQuery.extend(true, {}, that.oKeys);
that.oAllLengths = jQuery.extend(true, {}, that.oLengths);
that.oAllFinalLengths = jQuery.extend(true, {}, that.oFinalLengths);
delete that.mRequestHandles[sRequestKey];
that.bNeedsUpdate = true;
// apply clientside filters, if any
if ((that.aApplicationFilters && that.aApplicationFilters.length > 0) ||
(that.aFilters && that.aFilters.length > 0)) {
that._applyFilter();
}
// apply clientside sorters
if (that.aSorters && that.aSorters.length > 0) {
that._applySort();
}
that.oModel.callAfterUpdate(function() {
that.fireDataReceived({data: oData});
});
};
var fnError = function (oError) {
delete that.mRequestHandles[sRequestKey];
// handle error state like the ListBinding -> reset data and trigger update
var bAborted = oError.statusCode == 0;
if (!bAborted) {
that.oKeys = {};
that.oLengths = {};
that.oFinalLengths = {};
that.oAllKeys = {};
that.oAllLengths = {};
that.oAllFinalLengths = {};
that._fireChange({reason: ChangeReason.Change});
that.fireDataReceived();
}
};
// request the tree collection
if (!this.bSkipDataEvents) {
this.fireDataRequested();
}
this.bSkipDataEvents = false;
if (this.mRequestHandles[sRequestKey]) {
this.mRequestHandles[sRequestKey].abort();
}
var sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext());
if (sAbsolutePath) {
this.mRequestHandles[sRequestKey] = this.oModel.read(sAbsolutePath, {
urlParameters: aURLPa