UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

691 lines (611 loc) 21.6 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 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([ "./CountMode", "sap/base/Log", "sap/base/util/each", "sap/ui/model/TreeBinding" ], function(CountMode, Log, each, TreeBinding) { "use strict"; /** * * @class * Tree binding implementation for client models * * @param {sap.ui.model.Model} oModel * @param {string} sPath * @param {sap.ui.model.Context} oContext * @param {array} [aFilters] predefined filter/s (can be either a filter or an array of filters) * @param {object} [mParameters] * * @deprecated please use the <code>sap.ui.model.odata.v2.ODataModel</code> for improved tree binding support (e.g. filtering which is not supported in V1). * * @alias sap.ui.model.odata.ODataTreeBinding * @extends sap.ui.model.TreeBinding */ var ODataTreeBinding = TreeBinding.extend("sap.ui.model.odata.ODataTreeBinding", /** @lends sap.ui.model.odata.ODataTreeBinding.prototype */ { constructor : function(oModel, sPath, oContext, aFilters, mParameters){ TreeBinding.apply(this, arguments); this.bPendingRequest = false; this.oFinalLengths = {}; this.oLengths = {}; this.oKeys = {}; this.bNeedsUpdate = false; this.bHasTreeAnnotations = this._hasTreeAnnotations(); this.oRootContext = null; this.iNumberOfExpandedLevels = mParameters && mParameters.numberOfExpandedLevels; this.sCountMode = (mParameters && mParameters.countMode) || this.oModel.sDefaultCountMode; if (!this.bHasTreeAnnotations) { if (!mParameters || !mParameters.navigation) { Log.error("A navigation paths parameter object has to be defined"); this.oNavigationPaths = {}; } else { this.oNavigationPaths = mParameters.navigation; } } else { Log.warning("Tree hierarchy annotations are deprecated and may not work correctly with the sap.ui.model.odata.ODataModel." + " Please use the sap.ui.model.odata.v2.ODataModel (since version 1.28) instead which fully supports hierarchy annotations."); } } }); /** * Return root contexts for the tree. * * @param {int} iStartIndex The start index of the requested contexts * @param {int} iLength The requested amount of contexts * @param {int} [iThreshold] Unused * * @return {Array} The contexts array * @protected */ ODataTreeBinding.prototype.getRootContexts = function(iStartIndex, iLength, iThreshold) { var sNodeId = null, mRequestParameters = { numberOfExpandedLevels: this.iNumberOfExpandedLevels }, aRootContexts = [], bRequestRootContexts = true, that = this; if (this.bHasTreeAnnotations) { mRequestParameters.level = 0; if (!this.bDisplayRootNode) { mRequestParameters.level = 1; } } else { sNodeId = this.oModel.resolve(this.getPath(), this.getContext()); mRequestParameters.navPath = this._getNavPath(this.getPath()); if (mRequestParameters.numberOfExpandedLevels > 0) { var sAbsPath = sNodeId; for (var i = 0; i < mRequestParameters.numberOfExpandedLevels; i++) { var sNewNavPath = this._getNavPath(sAbsPath); mRequestParameters.navPath += "/" + sNewNavPath; sAbsPath += "/" + sNewNavPath; } } var bIsList = this.oModel.isList(this.sPath, this.getContext()); if (bIsList) { //We are bound to a collection which represents the first level this.bDisplayRootNode = true; } else { //We are bound to a single entity which represents the root context //Get the binding context for the root element, it is created if it doesn't exist yet bRequestRootContexts = false; this.oModel.createBindingContext(sNodeId, null, {expand: mRequestParameters.navPath }, function(oNewContext) { aRootContexts = [oNewContext]; if (that.oRootContext !== oNewContext) { that.oRootContext = oNewContext; that._processODataObject(oNewContext.getObject(), sNodeId, mRequestParameters.navPath); that.bNeedsUpdate = true; } }, this.bRefresh); this.bRefresh = false; } } if (bRequestRootContexts) { //If the root node should not be displayed, we assume that there is only one root node if (!this.bDisplayRootNode) { aRootContexts = this._getContextsForNodeId(sNodeId, 0, 1, 0, mRequestParameters); } else { aRootContexts = this._getContextsForNodeId(sNodeId, iStartIndex, iLength, iThreshold, mRequestParameters); } } if (!this.bDisplayRootNode && aRootContexts.length > 0) { this.oRootContext = aRootContexts[0]; aRootContexts = this.getNodeContexts(aRootContexts[0], iStartIndex, iLength, iThreshold); } return aRootContexts; }; /** * Return node contexts for the tree. * * @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] Unused * * @return {sap.ui.model.Context[]} the contexts array * @protected */ ODataTreeBinding.prototype.getNodeContexts = function(oContext, iStartIndex, iLength, iThreshold) { var sNodeId, mRequestParameters = {}; if (this.bHasTreeAnnotations) { var sDrilldownState = oContext.getProperty(this.oTreeProperties["hierarchy-drill-state-for"]); //If we have a leaf we cannot fetch any child nodes if (sDrilldownState == "leaf") { return []; } sNodeId = oContext.getProperty(this.oTreeProperties["hierarchy-node-for"]); 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 * * @param {Object} oContext the context element of the node * @return {boolean} true if node has children * * @public */ ODataTreeBinding.prototype.hasChildren = function(oContext) { if (!oContext) { return false; } if (this.bHasTreeAnnotations) { var sDrilldownState = oContext.getProperty(this.oTreeProperties["hierarchy-drill-state-for"]); return sDrilldownState === "expanded" || sDrilldownState === "collapsed"; } else { var sNavPath = this._getNavPath(oContext.getPath()); var sPathToChildren = oContext.getPath() + "/" + sNavPath; return sNavPath && this.oLengths[sPathToChildren] > 0; } }; /** * Returns the number of child nodes * * @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 vHierachyNode; if (!oContext) { if (this.oRootContext) { vHierachyNode = this.oRootContext.getProperty(this.oTreeProperties["hierarchy-node-for"]); } else { //Needs to be adapted if backend services by sFIn change vHierachyNode = "000000"; } } else { vHierachyNode = oContext.getProperty(this.oTreeProperties["hierarchy-node-for"]); } return this.oLengths[vHierachyNode]; } else { if (!oContext) { 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 absolute path to be loaded * @param {int} [iStartIndex=0] The first node to get the context of * @param {int} [iLength=iSizeLimit] The number of nodes to get the context of * @param {int} [iThreshold=0] Unused * @param {object} mParameters Additional parameters for this function * * @return {array} Array of contexts * * @private */ ODataTreeBinding.prototype._getContextsForNodeId = function(sNodeId, iStartIndex, iLength, iThreshold, mParameters) { var aContexts = [], bLoadContexts, sKey; // Set default values if startindex, threshold or length are not defined if (!iStartIndex) { iStartIndex = 0; } if (!iLength) { iLength = this.oModel.iSizeLimit; } if (!iThreshold) { iThreshold = 0; } if (this.bHasTreeAnnotations) { /*****************************/ /*** FIX for sFIN ******/ if (sNodeId == null) { sNodeId = "000000"; } if (mParameters.level == 0) { mParameters.level++; } } if (this.oFinalLengths[sNodeId] && this.oLengths[sNodeId] < iLength) { iLength = this.oLengths[sNodeId]; } // Loop through known data and check whether we already have all rows loaded if (this.oKeys[sNodeId]) { for (var i = iStartIndex; i < iStartIndex + iLength; i++) { sKey = this.oKeys[sNodeId][i]; if (!sKey) { break; } aContexts.push(this.oModel.getContext('/' + sKey)); } } bLoadContexts = aContexts.length != iLength && !(this.oFinalLengths[sNodeId] && aContexts.length >= this.oLengths[sNodeId]); // check if metadata are already available if (this.oModel.getServiceMetadata()) { // If rows are missing send a request if (!this.bPendingRequest && bLoadContexts) { var aParams = []; if (this.bHasTreeAnnotations) { if (mParameters.numberOfExpandedLevels > 0) { var iLevel = mParameters.level + mParameters.numberOfExpandedLevels; aParams.push("$filter=" + this.oTreeProperties["hierarchy-level-for"] + " le '0" + iLevel + "'"); } else { aParams.push("$filter=" + this.oTreeProperties["hierarchy-level-for"] + " eq '0" + mParameters.level + "' and " + this.oTreeProperties["hierarchy-parent-node-for"] + " eq '" + sNodeId + "'"); } } else if (mParameters.navPath) { aParams.push("$expand=" + mParameters.navPath); } this._loadSubNodes(sNodeId, iStartIndex, iLength, iThreshold, aParams, mParameters); } } return aContexts; }; ODataTreeBinding.prototype._getCountForNodeId = function(sNodeId, iStartIndex, iLength, iThreshold, mParameters) { var that = this; // 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) { 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 sPath; if (this.bHasTreeAnnotations) { sPath = this.oModel.resolve(this.getPath(), this.getContext()); aParams.push("$filter=" + this.oTreeProperties["hierarchy-parent-node-for"] + " eq '" + sNodeId + "'"); } else { sPath = sNodeId; } // Only send request, if path is defined if (sPath) { var sUrl = this.oModel._createRequestUrl(sPath + "/$count", null, aParams); var oRequest = this.oModel._createRequest(sUrl, "GET", false); // count needs other accept header oRequest.headers["Accept"] = "text/plain, */*;q=0.5"; // execute the request and use the metadata if available // (since $count requests are synchronous we skip the withCredentials here) this.oModel._request(oRequest, _handleSuccess, _handleError, undefined, undefined, this.oModel.getServiceMetadata()); } }; /** * Load list data from the server. * * @param {string} sNodeId The id of the node which sub nodes should be loaded * @param {number} iStartIndex The index of the first node to load * @param {number} iLength The number of nodes to load * @param {number} iThreshold Unused * @param {string[]} aParams A list containing additional query parameters * @param {object} mParameters An object containing a <code>navPath</code> */ ODataTreeBinding.prototype._loadSubNodes = function(sNodeId, iStartIndex, iLength, iThreshold, aParams, mParameters) { var that = this, bInlineCountRequested = false; if (iStartIndex || iLength) { aParams.push("$skip=" + iStartIndex + "&$top=" + iLength); } if (!that.bHasTreeAnnotations && !this.oFinalLengths[sNodeId] && (this.sCountMode == CountMode.Inline || this.sCountMode == CountMode.Both)) { aParams.push("$inlinecount=allpages"); bInlineCountRequested = true; } function fnSuccess(oData) { var oEntry, i; // Collecting contexts if (oData.results) { //Entity set if (!that.bHasTreeAnnotations) { // update length (only when the inline count was requested and is available) if (bInlineCountRequested && oData.__count) { that.oLengths[sNodeId] = parseInt(oData.__count); that.oFinalLengths[sNodeId] = true; } else if (that.oModel.isCountSupported()) { that._getCountForNodeId(sNodeId); } that.oKeys[sNodeId] = []; 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 { var mLastNodeIdIndices = {}; for (i = 0; i < oData.results.length; i++) { oEntry = oData.results[i]; sNodeId = oEntry[that.oTreeProperties["hierarchy-parent-node-for"]]; if (i == 0) { mLastNodeIdIndices[sNodeId] = iStartIndex; } else if (mLastNodeIdIndices[sNodeId] == undefined) { mLastNodeIdIndices[sNodeId] = 0; } if (!(sNodeId in that.oKeys)) { that.oKeys[sNodeId] = []; that._getCountForNodeId(sNodeId); } that.oKeys[sNodeId][mLastNodeIdIndices[sNodeId]] = that.oModel._getKey(oEntry); mLastNodeIdIndices[sNodeId]++; } } } else { //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); } } that.oRequestHandle = null; that.bPendingRequest = false; that.bNeedsUpdate = true; } function fnCompleted() { that.fireDataReceived(); } function fnError(oData) { that.oRequestHandle = null; that.bPendingRequest = false; that.fireDataReceived(); } function fnUpdateHandle(oHandle) { that.oRequestHandle = oHandle; } if (sNodeId) { if (!this.oFinalLengths[sNodeId]) { this.bPendingRequest = true; // execute the request and use the metadata if available this.fireDataRequested(); var sAbsolutePath; if (this.bHasTreeAnnotations) { sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext()); } else { sAbsolutePath = sNodeId; } this.oModel._loadData(sAbsolutePath, aParams, fnSuccess, fnError, false, fnUpdateHandle, fnCompleted); } } }; /** * Resets the current list data and length. * * @param {sap.ui.model.Context} [oContext] Only reset specific content matching the context * * @private */ ODataTreeBinding.prototype.resetData = function(oContext) { if (oContext) { //Only reset specific content var sPath = oContext.getPath(); delete this.oKeys[sPath]; delete this.oLengths[sPath]; delete this.oFinalLengths[sPath]; } else { this.oKeys = {}; this.oLengths = {}; this.oFinalLengths = {}; this.oRootContext = null; } }; /** * Refreshes the binding, checks whether the model data has been changed and fires change event * if this is the case. For server side models this should refetch the data from the server. * To update a control, even if no data has been changed, e.g. to reset a control after failed * validation, use the parameter <code>bForceUpdate</code>. * * @param {boolean} [bForceUpdate] * Update the bound control even if no data has been changed * @param {object} [mChangedEntities] * A map of changed entities * @param {string} [mEntityTypes] * Entity types; if this models entity type is contained, a change event will be fired * * @public */ ODataTreeBinding.prototype.refresh = function(bForceUpdate, mChangedEntities, mEntityTypes) { var bChangeDetected = false; if (!bForceUpdate) { if (mEntityTypes) { var sResolvedPath = this.oModel.resolve(this.sPath, this.oContext); var oEntityType = this.oModel.oMetadata._getEntityTypeByPath(sResolvedPath); if (oEntityType && (oEntityType.entityType in mEntityTypes)) { bChangeDetected = true; } } if (mChangedEntities && !bChangeDetected) { each(this.oKeys, function(i, aNodeKeys) { each(aNodeKeys, function(i, sKey) { if (sKey in mChangedEntities) { bChangeDetected = true; return false; } return true; }); if (bChangeDetected) { return false; } return true; }); } if (!mChangedEntities && !mEntityTypes) { // default bChangeDetected = true; } } if (bForceUpdate || bChangeDetected) { this.resetData(); this.bNeedsUpdate = false; this.bRefresh = true; this._fireChange(); } }; /** * Not functional. * * @param {sap.ui.model.Filter[]|sap.ui.model.Filter} aFilters Unused * * @returns {this} A reference to itself to allow chaining * @public */ ODataTreeBinding.prototype.filter = function(aFilters){ Log.warning("Filtering is currently not possible in the ODataTreeBinding"); return this; }; /** * Check whether this Binding would provide new values and in case it changed, * inform interested parties about this. * * @param {boolean} [bForceUpdate] * Whether a change event should be fired regardles of this bindings state * @param {object} [mChangedEntities] * A map of changed entities to check if an update is necessary. * */ ODataTreeBinding.prototype.checkUpdate = function(bForceUpdate, mChangedEntities){ var bChangeDetected = false; if (!bForceUpdate) { if (this.bNeedsUpdate || !mChangedEntities) { bChangeDetected = true; } else { each(this.oKeys, function(i, aNodeKeys) { each(aNodeKeys, function(i, sKey) { if (sKey in mChangedEntities) { bChangeDetected = true; return false; } return true; }); if (bChangeDetected) { return false; } return true; }); } } if (bForceUpdate || bChangeDetected) { this.bNeedsUpdate = false; this._fireChange(); } }; ODataTreeBinding.prototype._getNavPath = function(sPath) { //Check the last part of the path var sAbsolutePath = this.oModel.resolve(sPath, this.getContext()); if (!sAbsolutePath) { return undefined; } var aPathParts = sAbsolutePath.split("/"), sEntityName = aPathParts[aPathParts.length - 1], sNavPath; //Only if part contains "(" we are working on a specific entity with children var sCurrent = sEntityName.split("(")[0]; if (sCurrent && this.oNavigationPaths[sCurrent]) { //Replace context with subitems context sNavPath = this.oNavigationPaths[sCurrent]; } return sNavPath; }; ODataTreeBinding.prototype._processODataObject = function(oObject, sPath, sNavPath) { var aNavPath = [], that = this; if (sNavPath && sNavPath.indexOf("/") > -1) { aNavPath = sNavPath.split("/"); sNavPath = aNavPath[0]; aNavPath.splice(0,1); } var oRef = this.oModel._getObject(sPath); if (Array.isArray(oRef)) { this.oKeys[sPath] = oRef; this.oLengths[sPath] = oRef.length; this.oFinalLengths[sPath] = true; } if (sNavPath && oObject[sNavPath]) { if (Array.isArray(oRef)) { oRef.forEach(function(sRef) { var oObject = that.getModel().getData("/" + sRef); that._processODataObject(oObject, "/" + sRef + "/" + sNavPath, aNavPath.join("/")); }); } else if (typeof oRef === "object") { that._processODataObject(oObject, sPath + "/" + sNavPath, aNavPath.join("/")); } } }; ODataTreeBinding.prototype._hasTreeAnnotations = function() { var oModel = this.oModel, oMetadata = oModel.oMetadata, sAbsolutePath = oModel.resolve(this.getPath(), this.getContext()), oEntityType = oMetadata._getEntityTypeByPath(sAbsolutePath), sTreeAnnotationNamespace = oMetadata.mNamespaces["sap"], that = this; //List of all annotations that are required for the OdataTreebinding to work this.oTreeProperties = { "hierarchy-level-for": false, "hierarchy-parent-node-for": false, "hierarchy-node-for": false, "hierarchy-drill-state-for": false }; if (!oEntityType) { Log.fatal("EntityType for path " + sAbsolutePath + " could not be found."); return false; } //Check if all required proeprties are available each(oEntityType.property, function(iIndex, oProperty) { if (!oProperty.extensions) { return true; } each(oProperty.extensions, function(iIndex, oExtension) { var sName = oExtension.name; if (oExtension.namespace === sTreeAnnotationNamespace && sName in that.oTreeProperties && !that.oTreeProperties[sName]) { that.oTreeProperties[sName] = oProperty.name; } }); return true; }); var bMissing = false; each(this.oTreeProperties, function(iIndex, oTreeProperty) { if (!oTreeProperty) { bMissing = true; return false; } return true; }); return !bMissing; }; return ODataTreeBinding; });