@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,343 lines (1,178 loc) • 98.7 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/*eslint-disable max-len */
// Provides the OData model implementation of a tree binding
sap.ui.define([
"sap/base/assert",
"sap/base/Log",
"sap/base/util/deepExtend",
"sap/base/util/each",
"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"
], function(assert, Log, deepExtend, each, isEmptyObject, ChangeReason, Context, Filter,
FilterProcessor, FilterType, Sorter, SorterProcessor, TreeBinding, TreeBindingUtils,
CountMode, ODataUtils, OperationMode) {
"use strict";
const sClassName = "sap.ui.model.odata.v2.ODataTreeBinding";
/**
* 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 filters to be used initially with type {@link sap.ui.model.FilterType.Application}; call {@link #filter} to
* replace them
* @param {object} [mParameters]
* Map of binding parameters
* @param {boolean} [mParameters.transitionMessagesOnly=false]
* Whether the tree binding only requests transition messages from the back end. If messages
* for entities of this collection need to be updated, use
* {@link sap.ui.model.odata.v2.ODataModel#read} on the parent entity corresponding to the
* tree binding's context, with the parameter <code>updateAggregatedMessages</code> set to
* <code>true</code>.
* @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]
* Deprecated since 1.102.0, as {@link sap.ui.model.odata.OperationMode.Auto} is deprecated;
* the threshold that defines how many entries should be fetched at least by the binding if
* <code>operationMode</code> is set to <code>Auto</code>
* @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} or
* {@link sap.ui.model.odata.OperationMode.Client OperationMode.Client}
* @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 as of version 1.31.0</b>, use <code>groupId</code> instead
* @param {object} [mParameters.navigation]
* <b>Deprecated as of version 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 sorters used initially; call {@link #sort} to replace them
* @throws {Error} If one of the filters uses an operator that is not supported by the underlying model
* implementation or if the {@link sap.ui.model.Filter.NONE} filter instance is contained in
* <code>vFilters</code> together with other filters
*
* @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.147.0
*/
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 = undefined;
this.sRefreshGroupId = undefined;
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 = FilterProcessor.createNormalizeCache();
// check filter integrity
this.oModel.checkFilter(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"
// the internal operation mode might change, the external operation mode
// (this.sOperationMode) will always be the original value
this.bClientOperation = this.sOperationMode === OperationMode.Client;
// 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;
this.bTransitionMessagesOnly = !!this.mParameters.transitionMessagesOnly;
// Whether a refresh has been performed
this.bRefresh = false;
// the maximum value for the $top URL parameter in client mode
this.iMaximumTopValue = 5000;
}
});
/**
* 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"
};
/**
* Gets the request headers for a read request.
*
* @returns {Object<string, string>|undefined}
* The request headers for a read request, or <code>undefined</code> if no headers are required
*
* @private
*/
ODataTreeBinding.prototype._getHeaders = function () {
return this.bTransitionMessagesOnly ? {"sap-messages": "transientOnly"} : undefined;
};
/**
* Builds a node filter string.
* mParams.id holds the ID value for filtering on the hierarchy node.
*
* @param {object} mParams The filter params
* @returns {string} The filter to use with <code>$filter</code>
*
* @private
*/
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
*
* @param {string} sOperator The filter operator
* @param {number} iLevel The filter level
* @returns {string} The filter to use with <code>$filter</code>
*
* @private
*/
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.getResolvedPath();
if (sAbsolutePath) {
this.mRequestHandles[sRequestKey] = this.oModel.read(sAbsolutePath, {
groupId: sGroupId,
headers: this._getHeaders(),
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);
// _loadSingleRootNodeByNavigationProperties is only used if there are no tree
// annotations so "navigation"-mode is used which is is deprecated since 1.44 (see
// mParameters.navigation), so deep path isn't needed
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.odata.v2.Context[]}
* The root contexts for the tree
* @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.getResolvedPath();
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=0]
* The maximum number of contexts to read before and after the given range; with this,
* controls can prefetch data that is likely to be needed soon, e.g. when scrolling down in a
* table.
* @return {sap.ui.model.odata.v2.Context[]}
* The contexts of the child nodes for the given context
* @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) {
var iLength;
if (this.bHasTreeAnnotations) {
if (!oContext) {
return false;
}
var sDrilldownState = oContext.getProperty(
this.oTreeProperties["hierarchy-drill-state-for"]);
var sNodeKey = this.oModel.getKey(oContext);
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 children, 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;
}
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 {sap.ui.model.Context} 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
* The start index of the page
* @param {int} iLength
* The length of the page
* @param {int} iThreshold
* The additionally loaded entities
* @param {object} mRequestParameters
* The additional request parameters
* @param {string} mRequestParameters.navPath
* The navigation path
* @return {sap.ui.model.odata.v2.Context[]}
* Contexts for the given node ID
*
* @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;
}
}
return false;
// 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) {
const sDeepPath = this.oModel.resolveDeep(this.sPath, this.oContext)
+ sKey.slice(sKey.indexOf("("));
aContexts.push(this.oModel.getContext('/' + sKey, sDeepPath));
} 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++) {
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++) {
sKey = this.oKeys[sNodeId][i];
if (!sKey) {
if (!fnFindInLoadedSections(i)) {
aMissingSections = TreeBindingUtils.mergeSections(aMissingSections, {startIndex: i, length: 1});
}
}
}
}
// for initial loading of a node use this shortcut.
} else 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 sNodeId (it's an OData-Key) and resolve the correct
// hierarchy node property as a filter value;
// aMissingSections are only requested for known contexts, so deep path isn't needed
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 if (!this.oAllKeys
&& !this.mRequestHandles[ODataTreeBinding.REQUEST_KEY_CLIENT]) {
// 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
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.
*
* @private
*/
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.getResolvedPath();
// 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) {
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
let oHeaders;
const bSeparateCountRequest = this.sCountMode === CountMode.Request || this.sCountMode === CountMode.Both;
const sCountType = bSeparateCountRequest ? "/$count" : "";
if (this.sCountMode == CountMode.Inline || this.sCountMode == CountMode.InlineRepeat) {
aParams.push("$top=0");
aParams.push("$inlinecount=allpages");
oHeaders = this._getHeaders();
}
if (this.sCustomParams4CountRequest && bSeparateCountRequest) {
aParams.push(this.sCustomParams4CountRequest);
}
if (this.sCustomParams && !bSeparateCountRequest) {
aParams.push(this.sCustomParams);
}
// send the counting request
if (sAbsolutePath) {
this.oModel.read(sAbsolutePath + sCountType, {
headers: oHeaders,
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 <code>CountMode.Request</code>. Inlinecounts are appended directly
* when issuing a loading request.
*
* @param {string} sNodeId The node's ID
*
* @private
*/
ODataTreeBinding.prototype._getCountForNodeId = function(sNodeId) {
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) {
sAbsolutePath = this.getResolvedPath();
// 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) {
// If node ID is given the count is requested for a missing section whose context is already available,
// so deep path isn't needed
const oNodeContext = this.oModel.getContext("/" + sNodeId);
const sHierarchyNodeId = oNodeContext.getProperty(this.oTreeProperties["hierarchy-node-for"]);
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);
}
if (this.sCustomParams4CountRequest) {
aParams.push(this.sCustomParams4CountRequest);
}
// Only send request, if path is defined
if (sAbsolutePath) {
sGroupId = this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId;
this.oModel.read(sAbsolutePath + "/$count", {
// this.bTransitionMessagesOnly is not relevant for $count requests -> no sap-messages header
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
* @returns {Object<string,string>} Map 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
* @param {boolean} bSkipFirstNode
* Whether to skip the first node
* @returns {Object<string,string[]>|undefined}
* Map of parent and child keys or <code>undefined</code> when <code>aData</code> is empty
*
* @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;
}
return undefined;
};
/**
* 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 The node
* @param {string} sNewKey The new key
*
* @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, true);
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.getResolvedPath();
if (sAbsolutePath) {
sGroupId = this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId;
this.mRequestHandles[sRequestKey] = this.oModel.read(sAbsolutePath, {
headers: this._getHeaders(),
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
* @param {object} oRequestedSection
* The requested section
*
* @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) {
var oEntry, i;
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 (i = 0; i < oData.results.length; i++) {
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 (i = 0; i < oData.results.length; i++) {
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.getResolvedPath();
} 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, {
headers: this._getHeaders(),
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.
*
* @param {string[]} aURLParams Additional URL parameters
* @param {array} aResultPages=[] An array containing the result arrays with previously read data
* @param {int} iNodesReceived=0 The number of previously read data
*
* @private
*/
ODataTreeBinding.prototype._loadCompleteTreeWithAnnotations = function (aURLParams, aResultPages = [],
iNodesReceived = 0) {
var that = this;
var sRequestKey = ODataTreeBinding.REQUEST_KEY_CLIENT;
const aOriginalURLParameters = aURLParams.slice();
var fnSuccess = function (oData) {
// all nodes on root level -> save in this.oKeys[null] = [] (?)
if (oData.results && oData.results.length > 0) {
iNodesReceived += oData.results.length;
aResultPages.push(oData.results);
if (oData.__next || oData.results.length === that.iMaximumTopValue)