UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,327 lines (1,144 loc) 89.9 kB
/*! * 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