@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,284 lines (1,200 loc) • 63.8 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 mixin sap.ui.model.odata.v4.ODataParentBinding for classes extending sap.ui.model.Binding
//with dependent bindings
sap.ui.define([
"./Context",
"./ODataBinding",
"./lib/_Helper",
"sap/base/Log",
"sap/ui/base/SyncPromise",
"sap/ui/model/ChangeReason"
], function (Context, asODataBinding, _Helper, Log, SyncPromise, ChangeReason) {
"use strict";
/**
* A mixin for all OData V4 bindings with dependent bindings.
*
* @alias sap.ui.model.odata.v4.ODataParentBinding
* @extends sap.ui.model.odata.v4.ODataBinding
* @mixin
*/
function ODataParentBinding() {
// initialize members introduced by ODataBinding
asODataBinding.call(this);
// the aggregated query options
this.mAggregatedQueryOptions = {};
// whether the aggregated query options are processed the first time
this.bAggregatedQueryOptionsInitial = true;
this.mCanUseCachePromiseByChildPath = {};
// auto-$expand/$select: promises to wait until child bindings have provided
// their path and query options
this.aChildCanUseCachePromises = [];
// the child paths that are handled by the parent binding due to path reduction, see
// #fetchIfChildCanUseCache and ODLB#fetchDownloadUrl
this.mChildPathsReducedToParent = {};
// query options resulting from child bindings added when this binding already has data
this.mLateQueryOptions = undefined;
// counts the sent but not yet completed PATCHes
this.iPatchCounter = 0;
// whether all sent PATCHes have been successfully processed
this.bPatchSuccess = true;
this.oReadGroupLock = undefined; // see #createReadGroupLock
this.oRefreshPromise = null; // see #createRefreshPromise and #resolveRefreshPromise
this.oResumePromise = undefined; // see #getResumePromise
}
asODataBinding(ODataParentBinding.prototype);
var sClassName = "sap.ui.model.odata.v4.ODataParentBinding";
/**
* Find the context in the uppermost binding in the hierarchy that can be reached with an empty
* path.
*
* @param {sap.ui.model.odata.v4.Context} oContext
* The context of the caller
* @returns {sap.ui.model.odata.v4.Context}
* The context that can be reached through empty paths
*
* @private
*/
ODataParentBinding.prototype._findEmptyPathParentContext = function (oContext) {
if (this.sPath === "" && this.oContext.getBinding) {
return this.oContext.getBinding()._findEmptyPathParentContext(this.oContext);
}
return oContext;
};
/**
* Resumes this binding. The binding can again fire change events and initiate data service
* requests.
*
* @param {boolean} bAsPrerenderingTask
* Whether resume is done as a prerendering task
* @throws {Error}
* If this binding is relative to an {@link sap.ui.model.odata.v4.Context} or if it is an
* operation binding or if it is not suspended
*
* @private
* @see #suspend
*/
ODataParentBinding.prototype._resume = function (bAsPrerenderingTask) {
var that = this;
function doResume() {
that.bSuspended = false;
if (that.oResumePromise) {
that.resumeInternal(true);
that.oResumePromise.$resolve();
that.oResumePromise = undefined;
}
}
if (this.oOperation) {
throw new Error("Cannot resume an operation binding: " + this);
}
if (!this.isRoot()) {
throw new Error("Cannot resume a relative binding: " + this);
}
if (!this.bSuspended) {
throw new Error("Cannot resume a not suspended binding: " + this);
}
if (bAsPrerenderingTask) {
// wait one additional prerendering because resume itself starts in a prerendering task
this.createReadGroupLock(this.getGroupId(), true, 1);
// dependent bindings are only removed in a *new task* in ManagedObject#updateBindings
// => must only resume in prerendering task
this.oModel.addPrerenderingTask(doResume);
} else {
this.createReadGroupLock(this.getGroupId(), true);
doResume();
}
};
/**
* Decides whether the given query options can be fulfilled by this binding and merges them into
* this binding's <code>mAggregatedQueryOptions</code> if necessary.
*
* The query options cannot be fulfilled if there are conflicts. A conflict is an option other
* than <code>$expand</code>, <code>$select</code> and <code>$count</code> which has different
* values in the aggregate and the options to be merged. This is checked recursively.
*
* If the cache is already immutable the query options are aggregated into
* <code>mLateQueryOptions</code>. Then they also cannot be fulfilled if they contain a
* <code>$expand</code> using a collection-valued navigation property.
*
* Note: * is an item in <code>$select</code> and <code>$expand</code> just as others, that is
* it must be part of the array of items and one must not ignore the other items if * is
* provided. See "5.1.3 System Query Option $expand" and "5.1.4 System Query Option $select" in
* specification "OData Version 4.01. Part 2: URL Conventions".
*
* @param {object} mQueryOptions - The query options to be merged
* @param {string} sBaseMetaPath - This binding's meta path
* @param {boolean} bCacheImmutable - Whether the cache of this binding is immutable
* @param {boolean} [bIsProperty] - Whether the child is a property binding
* @returns {boolean} Whether the query options can be fulfilled by this binding
*
* @private
*/
ODataParentBinding.prototype.aggregateQueryOptions = function (mQueryOptions, sBaseMetaPath,
bCacheImmutable, bIsProperty) {
var mAggregatedQueryOptionsClone = _Helper.clone(
bCacheImmutable && this.mLateQueryOptions || this.mAggregatedQueryOptions),
that = this;
/*
* Recursively merges the given query options into the given aggregated query options.
*
* @param {object} mAggregatedQueryOptions
* The aggregated query options
* @param {object} mQueryOptions0
* The query options to merge into the aggregated query options
* @param {string} [sMetaPath]
* The meta path for the current $expand (only used if cache is immutable)
* @param {boolean} [bInsideExpand]
* Whether the given query options are inside a $expand
* @param {boolean} [bAdd]
* Whether to add the given query options because they are in a $expand that has not been
* aggregated yet
* @returns {boolean}
* Whether the query options can be fulfilled by this binding
*/
function merge(mAggregatedQueryOptions, mQueryOptions0, sMetaPath, bInsideExpand, bAdd) {
/*
* Recursively merges the expand path into the aggregated query options.
*
* @param {string} sExpandPath The expand path
* @returns {boolean} Whether the query options can be fulfilled by this binding
*/
function mergeExpandPath(sExpandPath) {
var bAddExpand = !mAggregatedQueryOptions.$expand[sExpandPath],
sExpandMetaPath = sMetaPath + "/" + sExpandPath;
if (bAddExpand) {
mAggregatedQueryOptions.$expand[sExpandPath] = {};
if (bCacheImmutable && that.oModel.getMetaModel()
.fetchObject(sExpandMetaPath).getResult().$isCollection) {
return false;
}
}
return merge(mAggregatedQueryOptions.$expand[sExpandPath],
mQueryOptions0.$expand[sExpandPath], sExpandMetaPath, true, bAddExpand);
}
/*
* Merges the select path into the aggregated query options.
*
* @param {string} sSelectPath The select path
* @returns {boolean} Whether the query options can be fulfilled by this binding
*/
function mergeSelectPath(sSelectPath) {
if (!mAggregatedQueryOptions.$select.includes(sSelectPath)) {
mAggregatedQueryOptions.$select.push(sSelectPath);
}
return true;
}
// Top-level all query options in the aggregate are OK, even if not repeated in the
// child. In a $expand the child must also have them (and the second loop checks that
// they're equal). Property bindings are an exception to this rule.
return (bIsProperty || !bInsideExpand
|| Object.keys(mAggregatedQueryOptions).every(function (sName) {
return sName in mQueryOptions0 || sName === "$count" || sName === "$expand"
|| sName === "$select" || sName === "$top";
}))
// merge $count, $expand and $select; check that all others equal the aggregate
&& Object.keys(mQueryOptions0).every(function (sName) {
switch (sName) {
case "$count":
if (mQueryOptions0.$top === 0 && mAggregatedQueryOptions.$select) {
return true; // see below: @see mCountQueryOptions => ignore
}
if (mQueryOptions0.$count) {
mAggregatedQueryOptions.$count = true;
}
return true;
case "$expand":
mAggregatedQueryOptions.$expand ??= {};
return Object.keys(mQueryOptions0.$expand).every(mergeExpandPath);
case "$select":
mAggregatedQueryOptions.$select ??= [];
if (mAggregatedQueryOptions.$top === 0) {
// Note: @see mCountQueryOptions => drop
// (w/o $top, all data is ready anyway, no $count needed)
delete mAggregatedQueryOptions.$count;
// ($select needs data and thus contradicts $top : 0)
delete mAggregatedQueryOptions.$top;
}
return mQueryOptions0.$select.every(mergeSelectPath);
case "$top":
if (mQueryOptions0.$top !== 0 || !mQueryOptions0.$count) {
return false; // not mCountQueryOptions => unsupported
}
if (!mAggregatedQueryOptions.$select) {
mAggregatedQueryOptions.$top = 0;
} // else: see above: @see mCountQueryOptions => ignore
return true;
default:
if (bAdd) {
mAggregatedQueryOptions[sName] = mQueryOptions0[sName];
return true;
}
return mQueryOptions0[sName] === mAggregatedQueryOptions[sName];
}
});
}
if (merge(mAggregatedQueryOptionsClone, mQueryOptions, sBaseMetaPath)) {
if (bCacheImmutable) {
this.mLateQueryOptions = mAggregatedQueryOptionsClone;
} else {
this.mAggregatedQueryOptions = mAggregatedQueryOptionsClone;
if (this.mLateQueryOptions) {
merge(this.mLateQueryOptions, mQueryOptions);
}
}
return true;
}
return false;
};
/**
* Attach event handler <code>fnFunction</code> to the 'patchCompleted' event of this binding.
*
* @param {function} fnFunction The function to call when the event occurs
* @param {object} [oListener] Object on which to call the given function
* @returns {this} <code>this</code> to allow method chaining
*
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.attachPatchCompleted = function (fnFunction, oListener) {
return this.attachEvent("patchCompleted", fnFunction, oListener);
};
/**
* Attach event handler <code>fnFunction</code> to the 'patchSent' event of this binding.
*
* @param {function} fnFunction The function to call when the event occurs
* @param {object} [oListener] Object on which to call the given function
* @returns {this} <code>this</code> to allow method chaining
*
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.attachPatchSent = function (fnFunction, oListener) {
return this.attachEvent("patchSent", fnFunction, oListener);
};
/**
* Changes this binding's parameters and refreshes the binding. Since 1.111.0, a list binding's
* header context is deselected, but (since 1.120.13) only if the binding parameter
* '$$clearSelectionOnFilter' is set and the '$filter' or '$search' parameter is changed. In all
* other cases, the caller of this method needs to evaluate whether the changed parameters
* invalidate the current selection and then deselect the header context if needed.
*
* If there are pending changes that cannot be ignored, an error is thrown. Use
* {@link #hasPendingChanges} to check if there are such pending changes. If there are, call
* {@link sap.ui.model.odata.v4.ODataModel#submitBatch} to submit the changes or
* {@link sap.ui.model.odata.v4.ODataModel#resetChanges} to reset the changes before calling
* {@link #changeParameters}.
*
* The parameters are changed according to the given map of parameters: Parameters with an
* <code>undefined</code> value are removed, the other parameters are set, and missing
* parameters remain unchanged.
*
* @param {object} mParameters
* Map of binding parameters, see {@link sap.ui.model.odata.v4.ODataModel#bindList} and
* {@link sap.ui.model.odata.v4.ODataModel#bindContext}
* @throws {Error} If
* <ul>
* <li> there are pending changes that cannot be ignored,
* <li> the binding is part of a
* {@link sap.ui.model.odata.v4.ODataListBinding#create deep create} because it is
* relative to a {@link sap.ui.model.odata.v4.Context#isTransient transient} context,
* <li> <code>mParameters</code> is missing, contains binding-specific or unsupported
* parameters, contains unsupported values, or contains the property "$expand" or
* "$select" when the model is in auto-$expand/$select mode.
* </ul>
* The following exceptions apply:
* <ul>
* <li> Since 1.90.0, binding-specific parameters are ignored if they are unchanged.
* <li> Since 1.93.0, string values for "$expand" and "$select" are ignored if they are
* unchanged; pending changes are ignored if all parameters are unchanged.
* <li> Since 1.97.0, pending changes are ignored if they relate to a
* {@link sap.ui.model.odata.v4.Context#isKeepAlive kept-alive} context of this binding.
* <li> Since 1.98.0, {@link sap.ui.model.odata.v4.Context#isTransient transient} contexts
* of a {@link #getRootBinding root binding} do not count as pending changes.
* <li> Since 1.108.0, {@link sap.ui.model.odata.v4.Context#delete deleted} contexts do not
* count as pending changes.
* </ul>
*
* @public
* @since 1.45.0
*/
ODataParentBinding.prototype.changeParameters = function (mParameters) {
var mBindingParameters = Object.assign({}, this.mParameters),
sChangeReason, // @see sap.ui.model.ChangeReason
aChangedParameters = [],
sKey,
that = this;
/*
* Updates <code>sChangeReason</code> depending on the given custom or system query option
* name:
* - "$filter" and "$search" cause <code>ChangeReason.Filter</code>,
* - "$orderby" causes <code>ChangeReason.Sort</code>,
* - default is <code>ChangeReason.Change</code>.
*
* The "strongest" change reason wins: Filter > Sort > Change.
*
* @param {string} sName
* The name of a custom or system query option
* @throws {Error}
* If name is "$expand" or "$select" when the model is in auto-$expand/$select mode
*/
function updateChangeReason(sName) {
if (that.oModel.bAutoExpandSelect && (sName === "$expand" || sName === "$select")) {
throw new Error("Cannot change " + sName
+ " parameter in auto-$expand/$select mode: "
+ JSON.stringify(mParameters[sName])
+ " !== " + JSON.stringify(mBindingParameters[sName]));
}
if (sName === "$filter" || sName === "$search") {
sChangeReason = ChangeReason.Filter;
} else if (sName === "$orderby" && sChangeReason !== ChangeReason.Filter) {
sChangeReason = ChangeReason.Sort;
} else {
sChangeReason ??= ChangeReason.Change;
}
aChangedParameters.push(sKey);
}
this.checkTransient();
if (!mParameters) {
throw new Error("Missing map of binding parameters");
}
for (sKey in mParameters) {
if (sKey.startsWith("$$")) {
if (this.isUnchangedParameter(sKey, mParameters[sKey])) {
continue; // ignore unchanged binding-specific parameters
}
throw new Error("Unsupported parameter: " + sKey);
}
if (mParameters[sKey] === undefined && mBindingParameters[sKey] !== undefined) {
updateChangeReason(sKey);
delete mBindingParameters[sKey];
} else if (mBindingParameters[sKey] !== mParameters[sKey]) {
updateChangeReason(sKey);
if (typeof mParameters[sKey] === "object") {
mBindingParameters[sKey] = _Helper.clone(mParameters[sKey]);
} else {
mBindingParameters[sKey] = mParameters[sKey];
}
}
}
if (sChangeReason) {
if (this.hasPendingChanges(true)) {
throw new Error("Cannot change parameters due to pending changes");
}
this.applyParameters(mBindingParameters, sChangeReason, aChangedParameters);
}
};
/**
* Checks whether the given context (or any context of this binding) may be kept alive.
*
* @param {sap.ui.model.odata.v4.Context} [oContext]
* A context of this binding
* @param {boolean} [bKeepAlive]
* Whether to keep the given context alive
* @throws {Error}
* If <code>oContext.setKeepAlive(bKeepAlive)</code> is not allowed for the given (or any)
* context
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding#checkKeepAlive
* @private
* @see sap.ui.model.odata.v4.Context#setKeepAlive
*/
/*
* Checks dependent bindings for updates or refreshes the binding if the resource path of its
* parent context changed.
*
* @returns {sap.ui.base.SyncPromise<void>}
* A promise which is resolved without a defined result when the check is finished, or
* rejected in case of an error
* @throws {Error}
* If called with parameters
*
* @private
*/
// @override sap.ui.model.odata.v4.ODataBinding#checkUpdateInternal
ODataParentBinding.prototype.checkUpdateInternal = function (bForceUpdate) {
var that = this;
function updateDependents() {
return SyncPromise.all(that.getDependentBindings().map(function (oDependentBinding) {
return oDependentBinding.checkUpdateInternal();
}));
}
if (bForceUpdate !== undefined) {
throw new Error("Unsupported operation: " + sClassName + "#checkUpdateInternal must not"
+ " be called with parameters");
}
return this.oCachePromise.then(function (oCache) {
if (oCache && that.bRelative) {
return that.fetchResourcePath(that.oContext).then(function (sResourcePath) {
if (oCache.getResourcePath() === sResourcePath) {
return updateDependents();
}
return that.refreshInternal(""); // entity of context changed
});
}
return updateDependents();
});
};
/**
* Creates the entity in the cache. If the binding doesn't have a cache, it forwards to the
* parent binding.
*
* @param {sap.ui.model.odata.v4.lib._GroupLock} oUpdateGroupLock
* The group ID to be used for the POST request
* @param {string|sap.ui.base.SyncPromise<string>} vCreatePath
* The path for the POST request or a SyncPromise that resolves with that path
* @param {string} sCollectionPath
* The absolute path to the collection in the cache where to create the entity
* @param {string} sTransientPredicate
* A (temporary) key predicate for the transient entity: "($uid=...)"
* @param {object} [oInitialData={}]
* The initial data for the created entity
* @param {boolean} bAtEndOfCreated
* Whether the newly created entity should be inserted after previously created entities, not
* before them
* @param {function} fnErrorCallback
* A function which is called with an error object each time a POST request for the create
* fails
* @param {function} fnSubmitCallback
* A function which is called just before a POST request for the create is sent
* @returns {sap.ui.base.SyncPromise<object>}
* 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
*
* @private
*/
ODataParentBinding.prototype.createInCache = function (oUpdateGroupLock, vCreatePath,
sCollectionPath, sTransientPredicate, oInitialData, bAtEndOfCreated, fnErrorCallback,
fnSubmitCallback) {
var that = this;
return this.oCachePromise.then(function (oCache) {
var sPathInCache;
if (oCache) {
sPathInCache = _Helper.getRelativePath(sCollectionPath, that.getResolvedPath());
return oCache.create(oUpdateGroupLock, vCreatePath, sPathInCache,
sTransientPredicate, oInitialData, bAtEndOfCreated, fnErrorCallback,
fnSubmitCallback
).then(function (oCreatedEntity) {
if (that.mCacheByResourcePath) {
// Ensure that cache containing non-transient created entity is recreated
// when the parent binding changes to another row and back again.
delete that.mCacheByResourcePath[oCache.getResourcePath()];
}
return oCreatedEntity;
});
}
return that.oContext.getBinding().createInCache(oUpdateGroupLock, vCreatePath,
sCollectionPath, sTransientPredicate, oInitialData, bAtEndOfCreated,
fnErrorCallback, fnSubmitCallback);
});
};
/**
* Creates a group lock and keeps it in this.oReadGroupLock.
* ODataListBinding#getContexts or ODataContextBinding#fetchValue are expected to use and remove
* it. To ensure that the queue does not remain locked forever the lock is unlocked and taken
* out again if it still resides there in the chosen prerendering.
*
* If not specified otherwise, the function removes the lock in the 2nd prerendering, because
* there are controls that render first before they request data from the model (for example the
* sap.ui.table.Table with VisibleRowCountMode=Auto).
*
* @param {string} [sGroupId]
* The group ID
* @param {boolean} [bLocked]
* Whether the group lock is locked
* @param {number} [iCount=0]
* The number of additional prerenderings to wait before removing a stale lock again
*
* @private
*/
ODataParentBinding.prototype.createReadGroupLock = function (sGroupId, bLocked, iCount) {
var oGroupLock,
that = this;
function addUnlockTask() {
that.oModel.addPrerenderingTask(function () {
if (that.oReadGroupLock === oGroupLock) {
iCount -= 1;
if (iCount > 0) {
// Use a promise to get out of the prerendering loop
Promise.resolve().then(addUnlockTask);
} else {
// It is still the same, unused lock
Log.debug("Timeout: unlocked " + oGroupLock, null, sClassName);
that.removeReadGroupLock();
}
}
});
}
this.removeReadGroupLock();
this.oReadGroupLock = oGroupLock = this.lockGroup(sGroupId, bLocked);
if (bLocked) {
iCount = 2 + (iCount || 0);
addUnlockTask();
}
};
/**
* Creates a promise for the refresh to be resolved by the binding's GET request.
*
* @param {boolean} bPreventBubbling
* Whether the dataRequested and dataReceived events related to the refresh must not be
* bubbled up to the model
* @returns {Promise<any>} The created promise
*
* @see #isRefreshWithoutBubbling
* @see #resolveRefreshPromise
* @private
*/
ODataParentBinding.prototype.createRefreshPromise = function (bPreventBubbling) {
var oPromise, fnResolve;
oPromise = new Promise(function (resolve) {
fnResolve = resolve;
});
oPromise.$preventBubbling = bPreventBubbling;
oPromise.$resolve = fnResolve;
this.oRefreshPromise = oPromise;
return oPromise;
};
/**
* Deletes the entity identified by the edit URL.
*
* @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 {sap.ui.model.odata.v4.Context} oContext
* The context to be deleted
* @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 {boolean} [bDoNotRequestCount]
* Whether not to request the new count from the server; useful in case of
* {@link sap.ui.model.odata.v4.Context#replaceWith} where it is known that the count remains
* unchanged; w/o a lock this should be true
* @param {function} fnUndelete
* A function to undelete the context (and poss. the context the deletion was delegated to)
* when the deletion failed or has been canceled
* @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.
* @throws {Error}
* If the cache promise for this binding is not yet fulfilled, or if the cache is shared
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding#delete
* @private
*/
/**
* Deletes the entity in the cache. If the binding doesn't have a cache, it forwards to the
* parent binding adjusting the path.
*
* @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 {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; returns <code>undefined</code> if the
* cache promise for this binding is not yet fulfilled
* @throws {Error}
* If the cache is shared
*
* @private
*/
ODataParentBinding.prototype.deleteFromCache = function (oGroupLock, sEditUrl, sPath,
oETagEntity, fnCallback) {
return this.withCache(function (oCache, sCachePath) {
return oCache._delete(oGroupLock, sEditUrl, sCachePath, oETagEntity, fnCallback);
}, sPath, /*bSync*/true);
};
/**
* Destroys the object. The object must not be used anymore after this function was called.
*
* @public
* @since 1.61.0
*/
ODataParentBinding.prototype.destroy = function () {
this.mLateQueryOptions = undefined;
this.removeReadGroupLock();
this.oRefreshPromise = undefined;
this.oResumePromise = undefined;
this.mCanUseCachePromiseByChildPath = undefined;
// cannot be set to undefined, might be modified after destruction
this.mAggregatedQueryOptions = {};
this.aChildCanUseCachePromises = [];
this.mChildPathsReducedToParent = {};
asODataBinding.prototype.destroy.call(this);
};
/**
* Detach event handler <code>fnFunction</code> from the 'patchCompleted' event of this binding.
*
* @param {function} fnFunction The function to call when the event occurs
* @param {object} [oListener] Object on which to call the given function
* @returns {this} <code>this</code> to allow method chaining
*
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.detachPatchCompleted = function (fnFunction, oListener) {
return this.detachEvent("patchCompleted", fnFunction, oListener);
};
/**
* Detach event handler <code>fnFunction</code> from the 'patchSent' event of this binding.
*
* @param {function} fnFunction The function to call when the event occurs
* @param {object} [oListener] Object on which to call the given function
* @returns {this} <code>this</code> to allow method chaining
*
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.detachPatchSent = function (fnFunction, oListener) {
return this.detachEvent("patchSent", fnFunction, oListener);
};
/**
* Handles exceptional cases of setting the property with the given path to the given value.
*
* @param {string} sPath
* A path (absolute or relative to this binding)
* @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
* @returns {sap.ui.base.SyncPromise<void>|undefined}
* <code>undefined</code> for the general case which is handled generically by the caller
* {@link sap.ui.model.odata.v4.Context#doSetProperty} or a <code>SyncPromise</code> for the
* exceptional case
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding#doSetProperty
* @private
*/
/**
* Binding specific code for suspend.
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding#doSuspend
* @private
*/
/**
* Determines whether a child binding with the given context and path can use
* the cache of this binding or one of its ancestor bindings. If this is the case, enhances
* the aggregated query options of this binding with the query options computed from the child
* binding's path; the aggregated query options initially hold the binding's local query
* options with the entity type's key properties added to $select.
*
* The decision is based on the reduced path of the child binding. If the resolved binding path
* contains a pair of navigation properties that are marked as partners, the path is reduced by
* removing these two navigation properties from the path.
*
* @param {sap.ui.model.odata.v4.Context} oContext
* A context of this binding which is the direct or indirect parent of the child binding.
* Initially it is the child binding's parent context (See
* {@link sap.ui.model.odata.v4.ODataBinding#fetchOrGetQueryOptionsForOwnCache}). When a
* binding delegates up to its parent binding, it passes its own parent context adjusting
* <code>sChildPath</code> accordingly.
* @param {string} sChildPath
* The child binding's binding path relative to <code>oContext</code>
* @param {object|sap.ui.base.SyncPromise<object>} [vChildQueryOptions={}]
* The child binding's (aggregated) query options (if any) or a promise resolving with them
* @param {boolean} [bIsProperty]
* Whether the child is a property binding
* @returns {sap.ui.base.SyncPromise<string|undefined>}
* A promise resolved with the reduced path for the child binding if the child binding can use
* this binding's or an ancestor binding's cache; resolved with <code>undefined</code>
* otherwise.
*
* @private
* @see sap.ui.model.odata.v4.ODataMetaModel#getReducedPath
*/
ODataParentBinding.prototype.fetchIfChildCanUseCache = function (oContext, sChildPath,
vChildQueryOptions, bIsProperty) {
// getBaseForPathReduction must be called early, because the (virtual) parent context may be
// lost again when the path is needed
var sBaseForPathReduction = this.getBaseForPathReduction(),
sBaseMetaPath,
bCacheImmutable,
oCanUseCachePromise,
// whether this binding is an operation or depends on one
bDependsOnOperation = oContext.getPath().includes("(...)"),
iIndex = oContext.iIndex,
bIsAdvertisement = sChildPath[0] === "#",
oMetaModel = this.oModel.getMetaModel(),
oParentContext = this.oContext, // Note: might disappear later on
aPromises,
sResolvedChildPath = this.oModel.resolve(sChildPath, oContext),
that = this;
/*
* Fetches the property that is reached by the calculated meta path and (if necessary) its
* type.
* @returns {sap.ui.base.SyncPromise<object|undefined>}
* A promise that is either resolved with the property or, in case of an action
* advertisement with the entity. If no property can be reached by the calculated meta
* path the promise is resolved with undefined.
*/
function fetchPropertyAndType() {
if (bIsAdvertisement) {
// Ensure entity type metadata is loaded even for advertisement so that sync access
// to key properties is possible
return oMetaModel.fetchObject(sBaseMetaPath + "/");
}
return _Helper.fetchPropertyAndType(that.oModel.oInterface.fetchMetadata,
getStrippedMetaPath(sResolvedChildPath));
}
/*
* Returns the meta path corresponding to the given path, with its annotation part stripped
* off.
*
* @param {string} sPath - A path
* @returns {string} The meta path with its annotation part stripped off
*/
function getStrippedMetaPath(sPath) {
var iIndex0;
sPath = _Helper.getMetaPath(sPath);
iIndex0 = sPath.indexOf("@"); // Note: sPath[0] !== "@"
return iIndex0 > 0 ? sPath.slice(0, iIndex0) : sPath;
}
if (bDependsOnOperation && !sResolvedChildPath.includes("/$Parameter/")
|| this.isRootBindingSuspended()
|| _Helper.isDataAggregation(this.mParameters)
&& (oContext === this.getHeaderContext?.() || oContext.isAggregated()
|| this.mParameters.$$aggregation.aggregate[sChildPath]?.name)) {
// In general there is no need for auto-$expand/$select, if the given context is a
// header context. But exiting here always, if a header context is used, changes the
// timing.
// With data aggregation, no auto-$expand/$select is needed for a header context, a
// context referencing aggregated data, or a dynamic property, but the child may still
// use the parent's cache; in case of a single record auto-$expand/$select is used.
// Note: Operation bindings do not support auto-$expand/$select yet
return SyncPromise.resolve(sResolvedChildPath);
}
// A binding w/o cache must skip this optimization and pass on to the parent binding;
// otherwise late properties might be missing later
oCanUseCachePromise = this.mCanUseCachePromiseByChildPath[sChildPath];
if (bIsProperty && this.oCache !== null && oCanUseCachePromise) {
return oCanUseCachePromise.then(function (sOldReducedPath) {
if (!sOldReducedPath) {
return undefined;
}
// Note: sResolvedChildPath could be "/SalesOrderList('42')/SO_2_SOITEM/0/Note"
// w/ index (thus getMetaPath helps), but getStrippedMetaPath makes no difference
if (!sChildPath.includes("/")
|| _Helper.getMetaPath(sOldReducedPath)
=== _Helper.getMetaPath(sResolvedChildPath)) {
return sResolvedChildPath;
}
return oMetaModel.getReducedPath(sResolvedChildPath, sBaseForPathReduction);
});
}
// Note: this.oCachePromise exists for all bindings except operation bindings; it might
// become pending again
bCacheImmutable = this.oCachePromise.isRejected()
|| iIndex !== undefined && iIndex !== Context.VIRTUAL
|| oContext.isEffectivelyKeptAlive() // no index when not in aContexts
|| this.oCache === null
|| this.oCache && this.oCache.hasSentRequest();
sBaseMetaPath = _Helper.getMetaPath(oContext.getPath());
aPromises = [
this.doFetchOrGetQueryOptions(oParentContext),
// After access to complete meta path of property, the metadata of all prefix paths
// is loaded so that synchronous access in wrapChildQueryOptions via getObject is
// possible - as well as #getReducedPath
fetchPropertyAndType(),
vChildQueryOptions
];
oCanUseCachePromise = SyncPromise.all(aPromises).then(function (aResult) {
var mChildQueryOptions = aResult[2] || {},
mCountQueryOptions,
mWrappedChildQueryOptions,
mLocalQueryOptions = aResult[0],
oProperty = aResult[1],
sReducedChildMetaPath,
sReducedPath;
if (Array.isArray(oProperty)) { // Arrays are only used for functions and actions
// a (non-deferred) function has to have its own cache
return undefined;
}
sReducedPath = oMetaModel.getReducedPath(sResolvedChildPath, sBaseForPathReduction);
sReducedChildMetaPath = _Helper.getRelativePath(getStrippedMetaPath(sReducedPath),
sBaseMetaPath);
if (sReducedChildMetaPath === undefined) {
// the child's data does not fit into this bindings's cache, try the parent
that.mChildPathsReducedToParent[sChildPath] = true;
return oParentContext.getBinding().fetchIfChildCanUseCache(oParentContext,
_Helper.getRelativePath(sResolvedChildPath, oParentContext.getPath()),
mChildQueryOptions, bIsProperty);
}
if (oProperty?.["@$ui5.$count"] && oContext !== that.getHeaderContext?.()
// avoid new $count handling in case of "manual" $expand
// Note: sChildPath.slice(0, -7) is the navigation property name
&& !mLocalQueryOptions.$expand?.[sChildPath.slice(0, -7)]) {
mCountQueryOptions = {
$expand : {
[sChildPath.slice(0, -7)] : {$count : true, $top : 0}
}
};
} else if (bDependsOnOperation || sReducedChildMetaPath === "$count"
|| sReducedChildMetaPath.endsWith("/$count")
|| sReducedChildMetaPath === "$selectionCount") {
return sReducedPath;
}
if (that.bAggregatedQueryOptionsInitial) {
that.mAggregatedQueryOptions = _Helper.clone(mLocalQueryOptions);
that.selectKeyProperties(that.mAggregatedQueryOptions, sBaseMetaPath);
that.bAggregatedQueryOptionsInitial = false;
}
if (bIsAdvertisement) {
mWrappedChildQueryOptions = {$select : [sReducedChildMetaPath.slice(1)]};
return that.aggregateQueryOptions(mWrappedChildQueryOptions, sBaseMetaPath,
bCacheImmutable, bIsProperty)
? sReducedPath
: undefined;
}
if (sReducedChildMetaPath === ""
|| oProperty
&& (oProperty.$kind === "Property" || oProperty.$kind === "NavigationProperty")) {
mWrappedChildQueryOptions = mCountQueryOptions
?? _Helper.wrapChildQueryOptions(sBaseMetaPath,
sReducedChildMetaPath, mChildQueryOptions,
that.oModel.oInterface.fetchMetadata);
if (mWrappedChildQueryOptions) {
return that.aggregateQueryOptions(mWrappedChildQueryOptions, sBaseMetaPath,
bCacheImmutable, bIsProperty)
? sReducedPath
: undefined;
}
return undefined;
}
if (sReducedChildMetaPath === "value") { // symbolic name for operation result
return that.aggregateQueryOptions(mChildQueryOptions, sBaseMetaPath,
bCacheImmutable, bIsProperty)
? sReducedPath
: undefined;
}
Log.error("Failed to enhance query options for auto-$expand/$select as the path '"
+ sResolvedChildPath + "' does not point to a property",
JSON.stringify(oProperty), sClassName);
return undefined;
}).then(function (sReducedPath) {
if (that.mLateQueryOptions && !that.isTransient()) {
if (that.oCache) {
that.oCache.setLateQueryOptions(that.mLateQueryOptions);
} else if (that.oCache === null) {
return oParentContext.getBinding()
.fetchIfChildCanUseCache(oParentContext, that.sPath, that.mLateQueryOptions)
.then(function (sPath) {
return sPath && sReducedPath;
});
}
}
return sReducedPath;
});
if (bIsProperty && this.oCache !== null && !oContext.getPath().includes("($uid=")) {
this.mCanUseCachePromiseByChildPath[sChildPath] = oCanUseCachePromise;
}
this.aChildCanUseCachePromises.push(oCanUseCachePromise);
// If the cache is immutable, only mLateQueryOptions may have changed
const oPromise = bCacheImmutable
? oCanUseCachePromise
: SyncPromise.all([this.oCachePromise, oCanUseCachePromise]).then(function (aResult) {
var oCache = aResult[0];
// Note: in operation bindings mAggregatedQueryOptions misses the options from
// $$inheritExpandSelect
if (oCache && !oCache.hasSentRequest() && !that.oOperation) {
if (that.bSharedRequest) {
oCache.setActive(false);
oCache = that.createAndSetCache(that.mAggregatedQueryOptions,
oCache.getResourcePath(), oContext);
} else {
oCache.setQueryOptions(_Helper.merge({}, that.oModel.mURLParameters,
that.mAggregatedQueryOptions));
}
}
return oCache;
});
// catch the error, but keep the rejected promise
oPromise.catch(function (oError) {
that.oModel.reportError(that + ": Failed to enhance query options for "
+ "auto-$expand/$select for child " + sChildPath, sClassName, oError);
});
if (!bCacheImmutable) {
this.oCachePromise = oPromise;
}
return oCanUseCachePromise;
};
/**
* Resolves the query options resulting from mParameters. Resolves all paths in $select
* containing navigation properties and converts them into an appropriate $expand if
* autoExpandSelect is active.
*
* @param {sap.ui.model.Context} oContext
* The context instance to be used
* @returns {sap.ui.base.SyncPromise<object>} A promise that resolves with the resolved
* query options when all paths in $select have been processed
*
* @private
* @see #getQueryOptionsFromParameters
*/
ODataParentBinding.prototype.fetchResolvedQueryOptions = function (oContext) {
var fnFetchMetadata,
mConvertedQueryOptions,
sMetaPath,
oModel = this.oModel,
mQueryOptions = this.getQueryOptionsFromParameters(),
that = this;
if (!(oModel.bAutoExpandSelect && mQueryOptions.$select)) {
return SyncPromise.resolve(mQueryOptions);
}
fnFetchMetadata = oModel.oInterface.fetchMetadata;
sMetaPath = _Helper.getMetaPath(oModel.resolve(this.sPath, oContext));
mConvertedQueryOptions = Object.assign({}, _Helper.clone(mQueryOptions), {$select : []});
return SyncPromise.all(mQueryOptions.$select.map(function (sSelectPath) {
var sMetaSelectPath = sMetaPath + "/" + sSelectPath;
if (sMetaSelectPath.endsWith(".*")) { // fetch metadata for namespace itself
sMetaSelectPath = sMetaSelectPath.slice(0, -1);
}
return _Helper.fetchPropertyAndType(fnFetchMetadata, sMetaSelectPath)
.then(function (oProperty) {
if (!oProperty && !sMetaSelectPath.endsWith("/*")) {
throw new Error("Invalid (navigation) property '" + sSelectPath
+ "' in $select of " + that);
}
const mWrappedQueryOptions = _Helper.wrapChildQueryOptions(
sMetaPath, sSelectPath, {}, fnFetchMetadata);
if (mWrappedQueryOptions) {
_Helper.aggregateExpandSelect(mConvertedQueryOptions, mWrappedQueryOptions);
} else {
_Helper.addToSelect(mConvertedQueryOptions, [sSelectPath]);
}
});
})).then(function () {
return mConvertedQueryOptions;
});
};
/**
* Finds the context that matches the given canonical path.
*
* @param {string} sCanonicalPath
* The canonical path of an entity (as a context path with the leading "/")
* @returns {sap.ui.model.odata.v4.Context}
* A matching context or <code>undefined</code> if there is none
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding#findContextForCanonicalPath
* @private
*/
/**
* Fire event 'patchCompleted' to attached listeners, if the last PATCH request is completed.
*
* @param {boolean} bSuccess Whether the current PATCH request has been processed successfully
*
* @private
*/
ODataParentBinding.prototype.firePatchCompleted = function (bSuccess) {
if (this.iPatchCounter === 0) {
throw new Error("Completed more PATCH requests than sent");
}
this.iPatchCounter -= 1;
this.bPatchSuccess &&= bSuccess;
if (this.iPatchCounter === 0) {
this.fireEvent("patchCompleted", {success : this.bPatchSuccess});
this.bPatchSuccess = true;
}
};
/**
* Fire event 'patchSent' to attached listeners, if the first PATCH request is sent.
*
* @private
*/
ODataParentBinding.prototype.firePatchSent = function () {
this.iPatchCounter += 1;
if (this.iPatchCounter === 1) {
this.fireEvent("patchSent");
}
};
/**
* Returns the absolute base path used for path reduction of child (property) bindings. This is
* the shortest possible path of a binding that may carry the data for the reduced path. A
* parent binding is not eligible if it uses a different update group with submit mode API.
*
* @returns {string}
* The absolute base path for path reduction
*
* @private
*/
ODataParentBinding.prototype.getBaseForPathReduction = function () {
var oParentBinding, sParentUpdateGroupId;
if (!this.isRoot()) {
oParentBinding = this.oContext.getBinding();
sParentUpdateGroupId = oParentBinding.getUpdateGroupId();
if (sParentUpdateGroupId === this.getUpdateGroupId()
|| !this.oModel.isApiGroup(sParentUpdateGroupId)) {
return oParentBinding.getBaseForPathReduction();
}
}
return this.getResolvedPath();
};
/**
* Returns the unique number of this bindings's generation, or <code>0</code> if it does not
* belong to any specific generation. This number can be inherited from a parent context.
*
* @returns {number}
* The unique number of this binding's generation, or <code>0</code>
*
* @private
*/
ODataParentBinding.prototype.getGeneration = function () {
return this.bRelative && this.oContext?.getGeneration?.() || 0;
};
/**
* Returns the query options that can be inherited from this binding, including late query
* options.
*
* @returns {object} The inheritable query options
*
* @private
*/
ODataParentBinding.prototype.getInheritableQueryOptions = function () {
if (this.mLateQueryOptions) {
return _Helper.merge({}, this.mCacheQueryOptions, this.mLateQueryOptions);
}
return this.mCacheQueryOptions
|| _Helper.getQueryOptionsForPath(
this.oContext.getBinding().getInheritableQueryOptions(), this.sPath);
};
/**
* Returns the query options for the given path relative to this binding. Uses the options
* resulting from the binding parameters or the options inherited from the parent binding by
* using {@link sap.ui.model.odata.v4.Context#getQueryOptionsForPath}.
*
* @param {string} sPath
* The relative path for which the query options are requested
* @param {sap.ui.model.Context} [oContext]
* The context that is used to compute the inherited query options; only relevant for the
* call from ODataListBinding#doCreateCache as this.oContext might not yet be set
* @returns {object}
* The computed query options (live reference, no clone!)
*
* @private
*/
ODataParentBinding.prototype.getQueryOptionsForPath = function (sPath, oContext) {
if (!_Helper.isEmptyObject(this.mParameters)) {
// binding has parameters -> all query options need to be defined at the binding
return _Helper.getQueryOptionsForPath(this.getQueryOptionsFromParameters(), sPath);
}
oContext ??= this.oContext;
// oContext is always set; as getQueryOptionsForPath is called only from ODLB#doCreateCache
// binding has no parameters -> no own query options
if (!this.bRelative || !oContext.getQueryOptionsForPath) {
// absolute or quasi-absolute -> no inheritance and no query options -> no options
return {};
}
return oContext.getQueryOptionsForPath(_Helper.buildPath(this.sPath, sPath));
};
/**
* Returns the query options resulting from mParameters. For operation bindings this includes
* $expand and $select from the parent context if the parameter $$inheritExpandSelect is set.
*
* Operation bindings directly use these options for the cache. With autoExpandSelect, other
* bindings may later extend these options to support child bindings that are allowed to
* participate in this binding's cache.
*
* @returns {object} The query options
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding.getQueryOptionsFromParameters
* @private
* @see sap.ui.model.odata.v4.ODataBinding#fetchOrGetQueryOptionsForOwnCache
* @see sap.ui.model.odata.v4.ODataBinding#doFetchOrGetQueryOptions
*/
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#getResumePromise
*/
ODataParentBinding.prototype.getResumePromise = function () {
return this.oResumePromise;
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#hasPendingChangesInDependents
*/
ODataParentBinding.prototype.hasPendingChangesInDependents = function (bIgnoreKeptAlive0,
sPathPrefix) {
return this.getDependentBindings().some(function (oDependent) {
var oCache = oDependent.oCache,
bHasPendingChanges,
bIgnoreKeptAlive = bIgnoreKeptAlive0; // new copy for this dependent only
if (bIgnoreKeptAlive) {
if (oDependent.oContext.isEffectivelyKeptAlive()) {
return false; // changes can be safely ignored here
}
if (oDependent.oContext.iIndex !== undefined) {
bIgnoreKeptAlive = false; // context of ODLB which is not kept alive: unsafe!
}
}
if (oCache !== undefined) {
// Pending changes for this cache are only possible when there is a cache already
if (oCache && oCache.hasPendingChangesForPath("", false, bIgnoreKeptAlive
&& oDependent.mParameters && oDependent.mParameters.$$ownRequest)) {
return true;
}
} else if (oDependent.hasPendingChangesForPath("")) {
return true;
}
if (oDependent.mCacheByResourcePath) {
bHasPendingChanges = Object.keys(oDependent.mCacheByResourcePath)
.some(function (sPath) {
var oCacheForPath = oDependent.mCacheByResourcePath[sPath];
return (!sPathPrefix || sPath.startsWith(sPathPrefix.slice(1)))
&& oCacheForPath !== oCache // don't ask again
&& oCacheForPath.hasPendingChangesForPath("");
});
if (bHasPendingChanges) {
return true;
}
}
return oDependent.hasPendingChangesInDependents(bIgnoreKeptAlive, sPathPrefix);
})
|| this.oModel.withUnresolvedBindings("hasPendingChangesInCaches",
this.getResolvedPath().slice(1));
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#isMeta
*/
ODataParentBinding.prototype.isMeta = function () {
return false;
};
/**
* Tells whether implicit loading of side effects via PATCH requests is switched off for this
* binding.
*
* @returns {boolean}
* Whether implicit loading of side effects is off
*
* @private
*/
ODataParentBinding.prototype.isPatchWithoutSideEffects = function () {
return this.mParameters.$$patchWithoutSideEffects
|