UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,276 lines (1,204 loc) 115 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ //Provides class sap.ui.model.odata.v4.Context sap.ui.define([ "./lib/_Helper", "sap/base/Log", "sap/ui/base/SyncPromise", "sap/ui/model/Context" ], function (_Helper, Log, SyncPromise, BaseContext) { "use strict"; var sClassName = "sap.ui.model.odata.v4.Context", // generation counter to distinguish old from new iGenerationCounter = 0, oModule, // index of virtual context used for auto-$expand/$select iVIRTUAL = Number.MIN_SAFE_INTEGER, // -9007199254740991 /** * @alias sap.ui.model.odata.v4.Context * @author SAP SE * @class Implementation of an OData V4 model's context. * * The context is a pointer to model data as returned by a query from an * {@link sap.ui.model.odata.v4.ODataContextBinding} or an * {@link sap.ui.model.odata.v4.ODataListBinding}. Contexts are always and only * created by such bindings. A context for a context binding points to the complete query * result. A context for a list binding points to one specific entry in the binding's * collection. A property binding does not have a context, you can access its value via * {@link sap.ui.model.odata.v4.ODataPropertyBinding#getValue}. * * Applications can access model data only via a context, either synchronously with the * risk that the values are not available yet ({@link #getProperty} and * {@link #getObject}) or asynchronously ({@link #requestProperty} and * {@link #requestObject}). * * Context instances are immutable except for their indexes. * @extends sap.ui.model.Context * @hideconstructor * @public * @since 1.39.0 * @version 1.147.0 */ Context = BaseContext.extend("sap.ui.model.odata.v4.Context", { constructor : constructor }); //********************************************************************************************* // Context //********************************************************************************************* /** * Do <strong>NOT</strong> call this private constructor. In the OData V4 model you cannot * create contexts at will: retrieve them from a binding or a view element instead. * * @param {sap.ui.model.odata.v4.ODataModel} oModel * The model * @param {sap.ui.model.odata.v4.ODataContextBinding|sap.ui.model.odata.v4.ODataListBinding} oBinding * A binding that belongs to the model * @param {string} sPath * An absolute path without trailing slash * @param {number} [iIndex] * Index of item (within the collection addressed by <code>sPath</code>) represented * by this context; used by list bindings, not context bindings * @param {sap.ui.base.SyncPromise<object>} [oCreatePromise] * A promise which is resolved with the created entity when the POST request has been * successfully sent and the entity has been marked as non-transient; used as base for * {@link #created} * @param {number} [iGeneration=0] * The unique number for this context's generation, which can be retrieved via * {@link #getGeneration} * @param {boolean} [bInactive] * Whether this context is inactive and will only be sent to the server after the first * property update * @throws {Error} * If an invalid path is given */ function constructor(oModel, oBinding, sPath, iIndex, oCreatePromise, iGeneration, bInactive) { if (sPath[0] !== "/") { throw new Error("Not an absolute path: " + sPath); } if (sPath.endsWith("/")) { throw new Error("Unsupported trailing slash: " + sPath); } BaseContext.call(this, oModel, sPath); this.oBinding = oBinding; this.setCreated(oCreatePromise); // a promise waiting for the deletion, also used as indicator for #isDeleted this.oDeletePromise = null; // avoids recursion when calling #doSetProperty within the createActivate event handler this.bFiringCreateActivate = false; this.iGeneration = iGeneration || 0; this.bInactive = bInactive || undefined; // be in sync with the annotation this.iIndex = iIndex; // this.iSelectionCount = 0; // on demand, for header contexts only this.bKeepAlive = false; // this.bOutdated = undefined; // on demand, for header contexts only; see #isOutdated this.bOutOfPlace = false; this.bSelected = false; this.fnOnBeforeDestroy = undefined; } /** * Adjusts this context's path by replacing the given transient predicate with the given * predicate. Recursively adjusts all child bindings. * * @param {string} sTransientPredicate * The transient predicate to be replaced * @param {string} sPredicate * The new predicate * @param {function} [fnPathChanged] * A function called with the old and the new path * * @private */ Context.prototype.adjustPredicate = function (sTransientPredicate, sPredicate, fnPathChanged) { var sTransientPath = this.sPath; this.sPath = sTransientPath.replace(sTransientPredicate, sPredicate); if (fnPathChanged) { fnPathChanged(sTransientPath, this.sPath); } this.oModel.getDependentBindings(this).forEach(function (oDependentBinding) { oDependentBinding.adjustPredicate(sTransientPredicate, sPredicate); }); }; /** * Updates all dependent bindings of this context. * * @private */ Context.prototype.checkUpdate = function () { if (this.oModel) { // might have already been destroyed this.oModel.getDependentBindings(this).forEach(function (oDependentBinding) { oDependentBinding.checkUpdate(); }); } }; /** * Updates all dependent bindings of this context. * * @returns {sap.ui.base.SyncPromise<void>} * A promise which is resolved without a defined result when the update is finished * @private */ Context.prototype.checkUpdateInternal = function () { return SyncPromise.all( this.oModel.getDependentBindings(this).map(function (oDependentBinding) { return oDependentBinding.checkUpdateInternal(); }) ); }; /** * Collapses the group node that this context points to. * * @param {boolean} [bAll] * Whether to collapse the node and all its descendants (since 1.132.0) * @throws {Error} * If the context points to a node that * <ul> * <li> is not expandable, * <li> is already collapsed, * <li> is a grand total, * </ul> * or if <code>bAll</code> is <code>true</code>, but no recursive hierarchy is present. * * @public * @see #expand * @see #isExpanded * @since 1.83.0 */ Context.prototype.collapse = function (bAll) { switch (this.getProperty("@$ui5.node.level") === 0 ? undefined : this.isExpanded()) { case true: this.oBinding.collapse(this, bAll); break; case false: throw new Error("Already collapsed: " + this); default: throw new Error("Not expandable: " + this); } }; /** * Returns a promise that is resolved without data when the entity represented by this context * has been created in the back end and all selected properties of this entity are available. * Expanded navigation properties are only available if the context's binding is refreshable. * {@link sap.ui.model.odata.v4.ODataContextBinding#refresh} and * {@link sap.ui.model.odata.v4.ODataListBinding#refresh} describe which bindings are * refreshable. * * As long as the promise is not yet resolved or rejected, the entity represented by this * context is transient. * * Once the promise is resolved, {@link #getPath} returns a path including the key predicate * of the new entity. This requires that all key properties are available. * * Note that the promise of a nested context within a deep create is always rejected, even if * the deep create succeeds. See {@link sap.ui.model.odata.v4.ODataListBinding#create} for more * details. * * @returns {Promise<void>|undefined} * A promise which is resolved without a defined result when the entity represented by this * context has been created in the back end. It is rejected with an <code>Error</code> * instance where <code>oError.canceled === true</code> if the transient entity is deleted * before it is created in the back end, for example via {@link #delete}, * {@link sap.ui.model.odata.v4.ODataListBinding#resetChanges} or * {@link sap.ui.model.odata.v4.ODataModel#resetChanges}, and for all nested contexts within a * deep create. It is rejected with an <code>Error</code> instance without * <code>oError.canceled</code> if loading of $metadata fails. Returns <code>undefined</code> * if the context has not been created using * {@link sap.ui.model.odata.v4.ODataListBinding#create}. * * @public * @since 1.43.0 */ Context.prototype.created = function () { return this.oCreatedPromise; }; /** * Deletes the OData entity this context points to. The context is removed from the binding * immediately, even if {@link sap.ui.model.odata.v4.SubmitMode.API} is used, and the request is * only sent later when {@link sap.ui.model.odata.v4.ODataModel#submitBatch} is called. As soon * as the context is deleted on the client, {@link #isDeleted} returns <code>true</code> and the * context must not be used anymore, especially not as a binding context. Exceptions hold for * status APIs like {@link #isDeleted}, {@link #isKeepAlive}, {@link #hasPendingChanges}, * {@link #resetChanges}, or {@link #isSelected} (returns <code>false</code> since 1.114.0). * * Since 1.105, such a pending deletion is a pending change. It causes * <code>hasPendingChanges</code> to return <code>true</code> for the context, the binding * containing it, and the model. The <code>resetChanges</code> method called on the context, the * binding, or the model cancels the deletion and restores the context. * * If the DELETE request succeeds, the context is destroyed and must not be used anymore. If it * fails or is canceled, the context is restored, reinserted into the list, and fully functional * again. * * If the deleted context is used as binding context of a control or view, the application is * advised to unbind it via * <code>{@link sap.ui.base.ManagedObject#setBindingContext setBindingContext(null)}</code> * before calling <code>delete</code>, and to possibly rebind it after reset or failure. The * model itself ensures that all bindings depending on this context become unresolved, but no * attempt is made to restore these bindings in case of reset or failure. * * Since 1.125.0, deleting a node in a recursive hierarchy (see * {@link sap.ui.model.odata.v4.ODataListBinding#setAggregation}) is supported. As a * precondition, the context must not be both {@link #setKeepAlive kept alive} and hidden (for * example due to a filter), and the group ID must not have * {@link sap.ui.model.odata.v4.SubmitMode.API}. Such a deletion is not a pending change. * * When using data aggregation without <code>groupLevels</code>, single entities can be deleted * (@experimental as of version 1.144.0, see {@link #isAggregated}). The same restrictions as * for a recursive hierarchy apply. * * @param {string} [sGroupId] * The group ID to be used for the DELETE request; if not specified, the update group ID for * the context's binding is used, see {@link #getUpdateGroupId}. Since 1.81, if this context * is transient (see {@link #isTransient}), no group ID needs to be specified. Since 1.98.0, * you can use <code>null</code> to prevent the DELETE request in case of a kept-alive context * that is not in the collection and of which you know that it does not exist on the server * anymore (for example, a draft after activation). Since 1.108.0, the usage of a group ID * with {@link sap.ui.model.odata.v4.SubmitMode.API} is possible. Since 1.121.0, you can use * the '$single' group ID to send a DELETE request as fast as possible; it will be wrapped in * a batch request as for a '$auto' group. * @param {boolean} [bDoNotRequestCount] * Whether not to request the new count from the server; useful in case of * {@link #replaceWith} where it is known that the count remains unchanged (since 1.97.0). * Since 1.98.0, this is implied if a <code>null</code> group ID is used. * @returns {Promise<void>} * A promise which is resolved without a defined result in case of success, or rejected with * an instance of <code>Error</code> in case of failure, for example if: * <ul> * <li> the given context does not point to an entity, * <li> the deletion on the server fails, * <li> the deletion is canceled via <code>resetChanges</code> (in this case the error * instance has the property <code>canceled</code> with value <code>true</code>). * </ul> * The error instance has the property <code>isConcurrentModification</code> with value * <code>true</code> in case a concurrent modification (e.g. by another user) of the entity * between loading and deletion has been detected; this should be shown to the user who needs * to decide whether to try deletion again. If the entity does not exist, we assume it has * already been deleted by someone else and report success. * @throws {Error} If * <ul> * <li> the given group ID is invalid, * <li> this context's root binding is suspended, * <li> a <code>null</code> group ID is used with a context which is not * {@link #isKeepAlive kept alive}, * <li> the context is already being deleted, * <li> the context's binding is a list binding with data aggregation, and either has group * levels or this context does not represent a single entity (see {@link #isAggregated}), * <li> the context is transient but its binding is not a list binding ("upsert") and it * therefore must be reset via {@link #resetChanges}, * <li> the restrictions for deleting from a recursive hierarchy or data aggregation (see * above) are not met. * </ul> * * @function * @public * @see #hasPendingChanges * @see #resetChanges * @see sap.ui.model.odata.v4.ODataContextBinding#hasPendingChanges * @see sap.ui.model.odata.v4.ODataListBinding#hasPendingChanges * @see sap.ui.model.odata.v4.ODataModel#hasPendingChanges * @see sap.ui.model.odata.v4.ODataContextBinding#resetChanges * @see sap.ui.model.odata.v4.ODataListBinding#resetChanges * @see sap.ui.model.odata.v4.ODataModel#resetChanges * @since 1.41.0 */ Context.prototype.delete = function (sGroupId, bDoNotRequestCount/*, bRejectIfNotFound*/) { var oEditUrlPromise, oGroupLock = null, that = this; if (this.isDeleted()) { throw new Error("Must not delete twice: " + this); } this.oBinding.checkSuspended(); // do it here even if it is contained in #isAggregated if (this.isAggregated() || this.oBinding.mParameters.$$aggregation?.groupLevels?.length) { throw new Error("Unsupported on aggregated data: " + this); } if (this.isTransient()) { if (!this.oBinding.getHeaderContext) { // upsert throw new Error("Cannot delete " + this); } if (this.iIndex === undefined) { return Promise.resolve(); // already deleted, nothing to do } sGroupId = null; } else if (sGroupId === null) { if (this.iIndex !== undefined || !this.isKeepAlive()) { throw new Error("Cannot delete " + this); } } if (this.oBinding.mParameters.$$aggregation) { if (this.iIndex === undefined) { throw new Error("Unsupported kept-alive context: " + this); } if (sGroupId !== null) { const sEffectiveGroupId = sGroupId ?? this.oBinding.getUpdateGroupId(); if (this.oModel.isApiGroup(sEffectiveGroupId)) { throw new Error("Unsupported group ID: " + sEffectiveGroupId); } } } if (sGroupId === null) { oEditUrlPromise = SyncPromise.resolve(); bDoNotRequestCount = true; } else { _Helper.checkGroupId(sGroupId, false, true); oEditUrlPromise = this.fetchCanonicalPath().then(function (sCanonicalPath) { return sCanonicalPath.slice(1); }); oGroupLock = this.oBinding.lockGroup(sGroupId, true, true); } return Promise.resolve( oEditUrlPromise.then(function (sEditUrl) { if (oGroupLock && that.oModel.getMetaModel().isAddressViaNavigationPath()) { sEditUrl = that.getPath().slice(1); } return that.oBinding.delete(oGroupLock, sEditUrl, that, /*oETagEntity*/null, bDoNotRequestCount, function () { // Note: may well be called twice when changes are canceled/reset! if (that.oDeletePromise) { that.oDeletePromise = null; if (that.bSelected) { that.oBinding.getHeaderContext().onSelectionChanged(that); } } } ); }).catch(function (oError) { if (oGroupLock) { oGroupLock.unlock(true); } throw oError; }) ); }; /** * Note: You may want to call {@link #delete} instead in order to delete the OData entity on the * server side. * * Destroys this context, that is, it removes this context from all dependent bindings and drops * references to {@link #getBinding binding} and {@link #getModel model}, so that the context * cannot be used anymore; it keeps path and index for debugging purposes. A destroyed context * can be recognized by calling {@link #getBinding}, which returns <code>undefined</code>. * * <b>BEWARE: Do not call this function!</b> The lifetime of an OData V4 context is completely * controlled by its binding. * * @public * @see sap.ui.base.Object#destroy * @since 1.41.0 */ // @override sap.ui.base.Object#destroy Context.prototype.destroy = function () { var fnOnBeforeDestroy = this.fnOnBeforeDestroy; if (fnOnBeforeDestroy) { // avoid second call through a destroy inside the callback this.fnOnBeforeDestroy = undefined; fnOnBeforeDestroy(); } this.oModel?.getDependentBindings(this).forEach(function (oDependentBinding) { oDependentBinding.setContext(undefined); }); this.oBinding = undefined; delete this.mChangeListeners; this.oCreatedPromise = undefined; // keep oDeletePromise so that isDeleted does not unexpectedly become false this.oSyncCreatePromise = undefined; this.bInactive = undefined; this.bKeepAlive = undefined; delete this.bOutdated; this.bSelected = false; // When removing oModel, ManagedObject#getBindingContext does not return the destroyed // context although the control still refers to it this.oModel = undefined; BaseContext.prototype.destroy.call(this); }; /** * Deletes the OData entity this context points to. * * @param {sap.ui.model.odata.v4.lib._GroupLock} [oGroupLock] * A lock for the group ID to be used for the DELETE request; w/o a lock, no DELETE is sent. * For a transient entity, the lock is ignored (use NULL)! * @param {string} [sEditUrl] * The entity's edit URL to be used for the DELETE request; only required with a lock * @param {string} sPath * The path of the entity relative to this binding * @param {object} [oETagEntity] * An entity with the ETag of the binding for which the deletion was requested. This is * provided if the deletion is delegated from a context binding with empty path to a list * binding. W/o a lock, this is ignored. * @param {sap.ui.model.odata.v4.ODataParentBinding} oBinding * The binding to perform the deletion at * @param {function} fnCallback * A function which is called immediately when an entity has been deleted from the cache, or * when it was re-inserted; the index of the entity and an offset (-1 for deletion, 1 for * re-insertion) are passed as parameter * @returns {sap.ui.base.SyncPromise<void>} * A promise which is resolved without a result in case of success, or rejected with an * instance of <code>Error</code> in case of failure * * @private * @see #delete */ Context.prototype.doDelete = function (oGroupLock, sEditUrl, sPath, oETagEntity, oBinding, fnCallback) { var oModel = this.oModel, that = this; this.oDeletePromise = oBinding.deleteFromCache( oGroupLock, sEditUrl, sPath, oETagEntity, fnCallback ).then(function () { var sResourcePathPrefix = that.sPath.slice(1); // Messages have been updated via _Cache#_delete; "that" is already destroyed; remove // all dependent caches in all bindings oModel.getAllBindings().forEach(function (oBinding0) { oBinding0.removeCachesAndMessages(sResourcePathPrefix, true); }); }).catch(function (oError) { oModel.reportError("Failed to delete " + that.getPath(), sClassName, oError); that.checkUpdate(); throw oError; }); if (this.bSelected) { this.oBinding.getHeaderContext().onSelectionChanged(this); } if (oGroupLock && oModel.isApiGroup(oGroupLock.getGroupId())) { oModel.getDependentBindings(this).forEach(function (oDependentBinding) { oDependentBinding.setContext(undefined); }); } return this.oDeletePromise; }; /** * Sets the new current value and updates the cache. * * @param {string} sPath * A path relative to this context * @param {any} vValue * The new value which must be primitive * @param {sap.ui.model.odata.v4.lib._GroupLock} [oGroupLock] * A lock for the group ID to be used for the PATCH request; without a lock, no PATCH is sent * @param {boolean} [bSkipRetry] * Whether to skip retries of failed PATCH requests and instead fail accordingly, but still * fire "patchSent" and "patchCompleted" events * @param {boolean} [bUpdating] * Whether the given property will not be overwritten by a creation POST(+GET) response * @returns {sap.ui.base.SyncPromise<void>} * A promise which is resolved without a result in case of success, or rejected with an * instance of <code>Error</code> in case of failure, for example if the annotation belongs to * the read-only namespace "@$ui5.*" * @throws {Error} If the context is deleted * * @private */ Context.prototype.doSetProperty = function (sPath, vValue, oGroupLock, bSkipRetry, bUpdating) { var oModel = this.oModel, oMetaModel = oModel.getMetaModel(), oPromise, oValue, that = this; if (this.isDeleted()) { if (oGroupLock) { oGroupLock.unlock(); } throw new Error("Must not modify a deleted entity: " + this); } if (sPath === "@$ui5.context.isSelected") { this.setSelected(vValue); if (oGroupLock) { oGroupLock.unlock(); oGroupLock = null; } if (this.oBinding.getHeaderContext?.() === this) { return SyncPromise.resolve(); } } if (oGroupLock && this.isTransient() && !this.isInactive()) { oValue = this.getValue(); oPromise = oValue && _Helper.getPrivateAnnotation(oValue, "transient"); if (oPromise instanceof Promise) { oGroupLock.unlock(); oGroupLock = oGroupLock.getUnlockedCopy(); this.doSetProperty(sPath, vValue, null, true, true) // early UI update .catch(oModel.getReporter()); return SyncPromise.resolve(oPromise).then(function (bSuccess) { // in case of success, wait until creation is completed because context path's // key predicate is adjusted return bSuccess && that.created(); }).then(function () { return that.doSetProperty(sPath, vValue, oGroupLock, bSkipRetry); }); } } if (oModel.bAutoExpandSelect) { sPath = oMetaModel.getReducedPath( oModel.resolve(sPath, this), this.oBinding.getBaseForPathReduction()); } return this.withCache(function (oCache, sCachePath, oBinding) { return oBinding.doSetProperty(sCachePath, vValue, oGroupLock) || oMetaModel.fetchUpdateData(sPath, that, !oGroupLock).then(function (oResult) { var sEntityPath = _Helper.getRelativePath(oResult.entityPath, oBinding.oReturnValueContext ? oBinding.oReturnValueContext.getPath() : oBinding.getResolvedPath()), // If a PATCH is merged into a POST request, firePatchSent is not called, // thus don't call firePatchCompleted bFirePatchCompleted = false; /* * Error callback to report the given error and fire "patchCompleted" * accordingly. * * @param {Error} oError */ function errorCallback(oError) { oModel.reportError("Failed to update path " + oModel.resolve(sPath, that), sClassName, oError); firePatchCompleted(false); } /* * Fire "patchCompleted" according to the given success flag, if needed. * * @param {boolean} bSuccess */ function firePatchCompleted(bSuccess) { if (bFirePatchCompleted) { oBinding.firePatchCompleted(bSuccess); bFirePatchCompleted = false; } } /* * Fire "patchSent" and remember to later fire "patchCompleted". */ function patchSent() { bFirePatchCompleted = true; oBinding.firePatchSent(); } if (!oGroupLock) { return oCache.setProperty(oResult.propertyPath, vValue, sEntityPath, bUpdating); } if (that.bInactive && !that.bFiringCreateActivate) { // early cache update so that the new value is properly available on the // event listener // runs synchronously - setProperty calls fetchValue with $cached oCache.setProperty(oResult.propertyPath, vValue, sEntityPath, bUpdating) .catch(oModel.getReporter()); that.bFiringCreateActivate = true; that.bInactive = oBinding.fireCreateActivate(that) ? false : 1; that.bFiringCreateActivate = false; oCache.setInactive(sEntityPath, that.bInactive); } const sEditUrl = oMetaModel.isAddressViaNavigationPath() ? oResult.entityPath.slice(1) : oResult.editUrl; const fnSetUpsertPromise = _Helper.hasPathSuffix(that.sPath, sEntityPath) ? that.setCreated.bind(that) : undefined; // if request is canceled fnPatchSent and fnErrorCallback are not called and // returned Promise is rejected -> no patch events return oCache.update(oResult.propertyPath, vValue, { sEditUrl : sEditUrl, sEntityPath : sEntityPath, fnErrorCallback : bSkipRetry ? undefined : errorCallback, oGroupLock : oGroupLock, fnIsKeepAlive : that.isEffectivelyKeptAlive.bind(that), fnPatchSent : patchSent, bPatchWithoutSideEffects : oBinding.isPatchWithoutSideEffects(), fnSetUpsertPromise : fnSetUpsertPromise, // Note: use that.oModel intentionally, fails if already destroyed! sUnitOrCurrencyPath : oMetaModel.getUnitOrCurrencyPath( that.oModel.resolve(sPath, that)) }).then(function () { firePatchCompleted(true); }, function (oError) { firePatchCompleted(false); throw oError; }); }); }, sPath, /*bSync*/false, /*bWithOrWithoutCache*/true); }; /** * Sets the selected state for this context. * * @param {boolean} bSelected * Whether this context is to be selected * @param {boolean} [bSilent] * Whether the client-side annotation "@$ui5.context.isSelected" should not be updated and * the binding should not be informed via * {@link sap.ui.model.odata.v4.ODataListBinding#onKeepAliveChanged} * @returns {boolean} * Whether the selection state of this context has changed * * @private * @see #setSelected */ Context.prototype.doSetSelected = function (bSelected, bSilent) { if (this.bSelected === bSelected) { return false; } this.bSelected = bSelected; if (!bSilent) { this.oBinding.onKeepAliveChanged(this); // selected contexts are effectively kept alive } const oHeaderContext = this.oBinding.getHeaderContext(); if (oHeaderContext === this) { if (!bSelected) { this.iSelectionCount = 0; // reset after previous "select all" } } else if (!this.isDeleted()) { // Note: deleted contexts can only be deselected, but are not currently counted anyway oHeaderContext.onSelectionChanged(this); // incl. "change" event for "$selectionCount" } // Note: data binding may cause #setSelected to be called redundantly! if (!bSilent) { this.withCache((oCache, sPath) => { if (this.oBinding) { oCache.setProperty("@$ui5.context.isSelected", bSelected, sPath); } // else: context already destroyed }, ""); } return true; // fire "selectionChanged" }; /** * Expands the group node that this context points to. Since 1.132.0, it is possible to do a * full expand, that is to expand all levels below a node, even if a node is already partially * or fully expanded. * * @param {boolean} [bAll] * Whether to expand the node and all its descendants (since 1.132.0) * @returns {Promise<void>} * A promise which is resolved without a defined result when the expand is successful, or * rejected in case of an error * @throws {Error} * If <code>bAll</code> is <code>true</code>, but no recursive hierarchy is present, or if the * context points to a node that is not expandable or is already expanded (unless a full * expand is requested). * * @public * @see #collapse * @see #isExpanded * @since 1.77.0 */ Context.prototype.expand = function (bAll) { switch (this.isExpanded()) { case true: if (!bAll) { throw new Error("Already expanded: " + this); } this.oBinding.collapse(this, /*bAll*/false, /*bSilent*/true); // fall through case false: { const iLevels = bAll ? Number.MAX_SAFE_INTEGER : 1; return Promise.resolve(this.oBinding.expand(this, iLevels)).then(() => {}); } default: throw new Error("Not expandable: " + this); } }; /** * Returns a promise for the "canonical path" of the entity for this context. * * @returns {sap.ui.base.SyncPromise<string>} * A promise which is resolved with the canonical path (e.g. "/SalesOrderList('0500000000')") * in case of success, or rejected with an instance of <code>Error</code> in case of failure, * e.g. if the given context does not point to an entity * * @private */ Context.prototype.fetchCanonicalPath = function () { return this.oModel.getMetaModel().fetchCanonicalPath(this); }; /** * Fetches and formats the primitive value at the given path. * * @param {string} sPath The requested path, absolute or relative to this context * @param {boolean} [bExternalFormat] * If <code>true</code>, the value is returned in external format using a UI5 type for the * given property path that formats corresponding to the property's EDM type and constraints. * @param {boolean} [bCached] * Whether to return cached values only and not initiate a request * @returns {sap.ui.base.SyncPromise<any>} a promise on the formatted value * * @private */ Context.prototype.fetchPrimitiveValue = function (sPath, bExternalFormat, bCached) { var oError, aPromises = [this.fetchValue(sPath, null, bCached)], sResolvedPath = this.oModel.resolve(sPath, this); if (bExternalFormat) { aPromises.push( this.oModel.getMetaModel().fetchUI5Type(sResolvedPath)); } return SyncPromise.all(aPromises).then(function (aResults) { var oType = aResults[1], vValue = aResults[0]; if (vValue && typeof vValue === "object") { oError = new Error("Accessed value is not primitive: " + sResolvedPath); oError.isNotPrimitive = true; throw oError; } return bExternalFormat ? oType.formatValue(vValue, "string") : vValue; }); }; /** * Delegates to the <code>fetchValue</code> method of this context's binding which requests * the value for the given path. A relative path is assumed to be relative to this context and * is reduced before accessing the cache if the model uses autoExpandSelect. * * @param {string} [sPath] * A path (absolute or relative to this context) * @param {sap.ui.model.odata.v4.ODataPropertyBinding} [oListener] * A property binding which registers itself as listener at the cache * @param {boolean} [bCached] * Whether to return cached values only and not initiate a request * @returns {sap.ui.base.SyncPromise<any>} * A promise on the outcome of the binding's <code>fetchValue</code> call; it is rejected * in case cached values are asked for, but not found * @throws {Error} If this context is a header context and no or empty path is given and * a listener is given. * * @private * @see #getObject * @see #getProperty */ Context.prototype.fetchValue = function (sPath, oListener, bCached) { if (this.iIndex === iVIRTUAL) { return SyncPromise.resolve(); // no cache access for virtual contexts } if (this.oBinding.getHeaderContext?.() === this) { const iSelectionCount = this.bSelected ? undefined // unknown : this.iSelectionCount ?? 0; if (sPath && sPath.startsWith(this.sPath)) { sPath = sPath.slice(this.sPath.length + 1); } if (!sPath) { if (oListener) { throw new Error("Cannot register change listener for header context object"); } return this.oBinding.fetchValue(this.sPath + "/$count", null, bCached) .then((iCount) => { return { ...(this.bOutdated !== undefined && {"@$ui5.context.isOutdated" : this.bOutdated}), "@$ui5.context.isSelected" : this.bSelected, $count : iCount, $selectionCount : iSelectionCount }; }); } if (sPath === "@$ui5.context.isSelected" || sPath === "@$ui5.context.isOutdated") { // @$ui5.context.isSelected and @$ui5.context.isOutdated are virtual properties // for header contexts and not part of the cache (in contrast to row contexts, // where they are saved in the cache). Therefore, change listeners are saved and // fired via the header context this.mChangeListeners ??= {}; _Helper.registerChangeListener(this, sPath, oListener); return SyncPromise.resolve(sPath === "@$ui5.context.isSelected" ? this.bSelected : this.bOutdated); } if (sPath === "$selectionCount") { this.mChangeListeners ??= {}; _Helper.registerChangeListener(this, sPath, oListener); return SyncPromise.resolve(iSelectionCount); } if (sPath !== "$count") { throw new Error("Invalid header path: " + sPath); } } if (!sPath) { sPath = this.sPath; } else if (sPath[0] !== "/") { // Create an absolute path based on the context's path and reduce it. This is only // necessary for data access via Context APIs, bindings already use absolute paths. sPath = this.oModel.resolve(sPath, this); if (this.oModel.bAutoExpandSelect) { sPath = this.oModel.getMetaModel() .getReducedPath(sPath, this.oBinding.getBaseForPathReduction()); } } return this.oBinding.fetchValue(sPath, oListener, bCached); }; /** * Returns the collection at the given path and removes it from the cache if it is marked as * transferable. * * @param {string} sPath - The relative path of the collection * @returns {object[]|undefined} The collection or <code>undefined</code> * @throws {Error} If the given path does not point to a collection. * * @private */ Context.prototype.getAndRemoveCollection = function (sPath) { return this.withCache(function (oCache, sCachePath) { return oCache.getAndRemoveCollection(sCachePath); }, sPath, true).unwrap(); }; /** * Returns the binding this context belongs to. * * @returns {sap.ui.model.odata.v4.ODataContextBinding|sap.ui.model.odata.v4.ODataListBinding} * The context's binding * * @public * @since 1.39.0 */ Context.prototype.getBinding = function () { return this.oBinding; }; /** * Returns the "canonical path" of the entity for this context. According to <a href= * "https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360932" * >"4.3.1 Canonical URL"</a> of the specification * "OData Version 4.01. Part 2: URL Conventions", this is the "name of the entity set associated * with the entity followed by the key predicate identifying the entity within the collection". * Use the canonical path in {@link sap.ui.core.Element#bindElement} to create an element * binding. * * Note: For a transient context (see {@link #isTransient}) a wrong path is returned unless all * key properties are available within the initial data. * * @returns {string} * The canonical path (e.g. "/SalesOrderList('0500000000')") * @throws {Error} * If the canonical path cannot be determined yet or in case of failure, e.g. if the given * context does not point to an entity * * @function * @public * @see #requestCanonicalPath * @since 1.39.0 */ Context.prototype.getCanonicalPath = _Helper.createGetMethod("fetchCanonicalPath", true); /** * Returns a filter object corresponding to this context. For an ordinary row context of a list * binding, the filter matches exactly the entity's key properties. For a subtotal row (see * {@link sap.ui.model.odata.v4.ODataListBinding.setAggregation}), the filter matches exactly * the groupable properties corresponding to this context. For a grand total, <code>null</code> * is returned. * * @returns {sap.ui.model.Filter|null} * A filter object corresponding to this context * @throws {Error} If this context is * <ul> * <li> not a list binding's row context, * <li> currently transient, * <li> using key aliases, * <li> using an index, not a key predicate in the last segment of its path, * <li> just created via {@link sap.ui.model.odata.v4.ODataModel#getKeepAliveContext} and * metadata is not yet available * </ul> * * @public * @since 1.130.0 */ Context.prototype.getFilter = function () { if (!this.oBinding.getHeaderContext || this.isTransient()) { throw new Error("Not a list context path to an entity: " + this); } const iPredicateIndex = _Helper.getPredicateIndex(this.sPath); const sPredicate = this.sPath.slice(iPredicateIndex).replace(/,?\$isTotal=true\)$/, ")"); if (sPredicate === "()") { return null; // grand total } const oMetaModel = this.oModel.getMetaModel(); const sMetaPath = _Helper.getMetaPath(this.sPath); const oEntityType = oMetaModel.getObject(sMetaPath + "/"); return _Helper.getFilterForPredicate(sPredicate, oEntityType, oMetaModel, sMetaPath, true); }; /** * Returns the unique number of this context's generation, or <code>0</code> if it does not * belong to any specific generation. This number can be inherited from a parent binding. * * @param {boolean} [bOnlyLocal] * Whether the local generation w/o inheritance is returned * @returns {number} * The unique number of this context's generation, or <code>0</code> * * @private * @see sap.ui.model.odata.v4.Context.createNewContext * @see #setNewGeneration */ Context.prototype.getGeneration = function (bOnlyLocal) { if (this.iGeneration || bOnlyLocal) { return this.iGeneration; } return this.oBinding.getGeneration(); }; /** * Returns the group ID of the context's binding that is used for read requests. See * {@link sap.ui.model.odata.v4.ODataListBinding#getGroupId} and * {@link sap.ui.model.odata.v4.ODataContextBinding#getGroupId}. * * @returns {string} * The group ID * * @public * @since 1.81.0 */ Context.prototype.getGroupId = function () { return this.oBinding.getGroupId(); }; /** * Returns the context's index within the binding's collection. The return value changes when a * new entity is added via {@link sap.ui.model.odata.v4.ODataListBinding#create} without * <code>bAtEnd</code>, and when a context representing a created entity is deleted again. * * @returns {number|undefined} * The context's index within the binding's collection. It is <code>undefined</code> if * <ul> * <li> it does not belong to a list binding, * <li> it is {@link #isKeepAlive kept alive}, but not in the collection currently. * </ul> * * @public * @since 1.39.0 */ // DO NOT call this function internally, use iIndex instead! Context.prototype.getIndex = function () { return this.iIndex !== undefined && this.oBinding ? this.oBinding.getViewIndex(this) : this.iIndex; }; /** * Returns the value for the given path relative to this context. The function allows access to * the complete data the context points to (if <code>sPath</code> is "") or any part thereof. * The data is a JSON structure as described in * <a href="https://docs.oasis-open.org/odata/odata-json-format/v4.01/"> * "OData JSON Format Version 4.01"</a>. * Note that the function clones the result. Modify values via * {@link sap.ui.model.odata.v4.ODataPropertyBinding#setValue}. * * Returns <code>undefined</code> if the data is not (yet) available; no request is initiated. * Use {@link #requestObject} for asynchronous access. * * The header context of a list binding only delivers <code>$count</code> and * <code>@$ui5.context.isSelected</code> (wrapped in an object if <code>sPath</code> is ""). * * @param {string} [sPath=""] * A path relative to this context * @returns {any} * The requested value * @throws {Error} * If the context's root binding is suspended or if the context is a header context and the * path is neither empty, "$count", nor "@ui5.context.isSelected". * * @public * @see sap.ui.model.Context#getObject * @since 1.39.0 */ // @override sap.ui.model.Context#getObject Context.prototype.getObject = function (sPath) { return _Helper.publicClone(this.getValue(sPath)); }; /** * Returns the parent node (in case of a recursive hierarchy; see * {@link sap.ui.model.odata.v4.ODataListBinding#setAggregation}) or * <code>undefined</code> if the parent of this node hasn't been read yet; it can then be * requested via {@link #requestParent}. * * @returns {sap.ui.model.odata.v4.Context|null|undefined} * The parent node, or <code>null</code> if this node is a root node and thus has no parent, * or <code>undefined</code> if the parent node hasn't been read yet * @throws {Error} If * <ul> * <li> this context is not a list binding's context, * <li> this context is not part of a recursive hierarchy. * </ul> * * @public * @since 1.122.0 */ Context.prototype.getParent = function () { if (!this.oBinding.fetchOrGetParent) { throw new Error("Not a list binding's context: " + this); } return this.oBinding.fetchOrGetParent(this); }; /** * Returns the property value for the given path relative to this context. The path is expected * to point to a structural property with primitive type. Returns <code>undefined</code> * if the data is not (yet) available; no request is initiated. Use {@link #requestProperty} * for asynchronous access. * * @param {string} sPath * A path relative to this context * @param {boolean} [bExternalFormat] * If <code>true</code>, the value is returned in external format using a UI5 type for the * given property path that formats corresponding to the property's EDM type and constraints. * If the type is not yet available, <code>undefined</code> is returned. * @returns {any} * The requested property value * @throws {Error} If * <ul> * <li> the context's root binding is suspended, * <li> the value is not primitive, * <li> or the context is a header context and the path is neither "$count", * "@ui5.context.isOutdated", nor "@ui5.context.isSelected". * </ul> * * @public * @see sap.ui.model.Context#getProperty * @see sap.ui.model.odata.v4.ODataMetaModel#requestUI5Type * @since 1.39.0 */ // @override sap.ui.model.Context#getProperty Context.prototype.getProperty = function (sPath, bExternalFormat) { var oError, oSyncPromise; this.oBinding.checkSuspended(); oSyncPromise = this.fetchPrimitiveValue(sPath, bExternalFormat, true); if (oSyncPromise.isRejected()) { oSyncPromise.caught(); oError = oSyncPromise.getResult(); if (oError.isNotPrimitive) { throw oError; } else if (!oError.$cached) { // Note: errors due to data requests have already been logged Log.warning(oError.message, sPath, sClassName); } } return oSyncPromise.isFulfilled() ? oSyncPromise.getResult() : undefined; }; /** * Returns the query options from the associated binding for the given path. * * @param {string} sPath * The relative path for which the query options are requested * @returns {object} * The query options from the associated binding (live reference, no clone!) * * @private */ Context.prototype.getQueryOptionsForPath = function (sPath) { return this.oBinding.getQueryOptionsForPath(sPath); }; /** * Returns this node's sibling; either the next one (via offset +1) or the previous one (via * offset -1). Returns <code>null</code> if no such sibling exists (because this node is the * last or first sibling, respectively). If it's not known whether the requested sibling * exists, <code>undefined</code> is returned and {@link #requestSibling} can be used instead. * * @param {number} [iOffset=+1] - An offset, either -1 or +1 * @returns {sap.ui.model.odata.v4.Context|null|undefined} * The sibling's context, or <code>null</code> if no such sibling exists for sure, or * <code>undefined</code> if we cannot tell * @throws {Error} If * <ul> * <li> the given offset is unsupported, * <li> this context's root binding is suspended, * <li> this context is {@link #isDeleted deleted}, {@link #isTransient transient}, or not * part of a recursive hierarchy. * </ul> * * @private * @since 1.126.0 * @ui5-restricted sap.fe */ Context.prototype.getSibling = function (iOffset) { return this.oBinding.fetchOrGetSibling(this, iOffset); }; /** * Returns the group ID of the context's binding that is used for update requests. See * {@link sap.ui.model.odata.v4.ODataListBinding#getUpdateGroupId} and * {@link sap.ui.model.odata.v4.ODataContextBinding#getUpdateGroupId}. * * @returns {string} * The update group ID * * @public * @since 1.81.0 */ Context.prototype.getUpdateGroupId = function () { return this.oBinding.getUpdateGroupId(); }; /** * Returns the value for the given path. The function allows access to the complete data the * context points to (if <code>sPath</code> is "") or any part thereof. The data is a JSON * structure as described in * <a href="https://docs.oasis-open.org/odata/odata-json-format/v4.01/"> * "OData JSON Format Version 4.01"</a>. * Note that the function returns the cache instance. Do not modify the result, use * {@link sap.ui.model.odata.v4.ODataPropertyBinding#setValue} instead. * * Returns <code>undefined</code> if the data is not (yet) available; no request is initiated. * * @param {string} [sPath=""] * A path, absolute or relative to this context * @returns {any} * The requested value * @throws {Error} * If the context's root binding is suspended * * @private */ Context.prototype.getValue = function (sPath) { var oSyncPromise, that = this; this.oBinding.checkSuspended(); oSyncPromise = this.fetchValue(sPath, null, true) .catch(function (oError) { if (!oError.$cached) { that.oModel.reportError("Unexpected error", sClassName, oError); } }); if (oSyncPromise.isFulfilled()) { return oSyncPromise.getResult(); } }; /** * Returns whether there are pending changes for bindings dependent on this context, or for * unresolved bindings (see {@link sap.ui.model.Binding#isResolved}) which were dependent on * this context at the time the pending change was created. This includes the context itself * being {@link #isTransient transient} or {@link #delete deleted} on the client, but not yet on * the server. Since 1.98.0, {@link #isInactive inactive} contexts are ignored, unless their * {@link sap.ui.model.odata.v4.ODataListBinding#event:createActivate activation} has been * prevented and therefore {@link #isInactive} returns <code>1</code>. * * @returns {boolean} * Whether there are pending changes * * @public * @since 1.53.0 */ Context.prototype.hasPendingChanges = function () { var that = this; return this.isTransient() && this.isInactive() !== true || this.oDeletePromise?.isPending() || this.oBinding.hasPendingChangesForPath(this.sPath) || this.oModel.getDependentBindings(this).some(function (oDependentBinding) { return oDependentBinding.oCache ? oDependentBinding._hasPendingChanges(false, that.sPath) : oDependentBinding.hasPendingChangesInDependents(false, that.sPath); }) || this.oModel.withUnresolvedBindings("hasPendingChangesInCaches", this.sPath.slice(1)); }; /** * Indicates whether this context represents aggregated data rather than a single entity * instance. This method returns <code>true</code> only in case of data aggregation (but not for * a recursive hierarchy) and not for non-expandable nodes (so-called leaves; see * {@link #isExpanded}) if all of the entity type's key properties are available as groups. For * a list binding's * {@link sap.ui.model.odata.v4.ODataListBinding#getHeaderContext header context}, the returned * value matches that of every leaf. * * @returns {boolean} Whether this context represents aggregated data * @throws {Error} If this context's root binding is suspended * * @public * @see sap.ui.model.odata.v4.ODataListBinding#setAggregation * @since 1.144.0 */ Context.prototype.isAggregated = function () { this.oBinding.checkSuspended(); // see _AggregationCache#isAggregated const bAggregated = this.oBinding.mParameters.$$aggregation?.$leafLevelAggregated; if (bAggregated === undefined) { return false; } // Note: #isExpanded fails for header context ("Invalid header path: @$ui5.node.isExpanded") return bAggregated || this.oBinding.getHeaderContext() !== this && this.isExpanded() !== undefined; }; /** * Tells whether this node is an ances