@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,276 lines (1,204 loc) • 115 kB
JavaScript
/*!
* 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