@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,310 lines (1,219 loc) • 57.8 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 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;
// auto-$expand/$select: promises to wait until child bindings have provided
// their path and query options
this.aChildCanUseCachePromises = [];
// whether the binding has a child with a path to the parent binding via path reduction
this.bHasPathReductionToParent = false;
// 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";
/**
* 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);
};
/**
* 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);
};
/**
* 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|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.
*
* @private
*/
ODataParentBinding.prototype.doSuspend = function () {
// nothing to do here
};
/**
* 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
*
* @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 = this.bPatchSuccess && bSuccess;
if (this.iPatchCounter === 0) {
this.fireEvent("patchCompleted", {success : this.bPatchSuccess});
this.bPatchSuccess = true;
}
};
/**
* 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);
};
/**
* 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);
};
/**
* 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");
}
};
/**
* 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;
};
/**
* 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.2 System Query Option $expand" and "5.1.3 System Query Option $select" in
* specification "OData Version 4.0 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.merge({},
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
* @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";
}))
// merge $count, $expand and $select; check that all others equal the aggregate
&& Object.keys(mQueryOptions0).every(function (sName) {
switch (sName) {
case "$count":
if (mQueryOptions0.$count) {
mAggregatedQueryOptions.$count = true;
}
return true;
case "$expand":
mAggregatedQueryOptions.$expand = mAggregatedQueryOptions.$expand || {};
return Object.keys(mQueryOptions0.$expand).every(mergeExpandPath);
case "$select":
mAggregatedQueryOptions.$select = mAggregatedQueryOptions.$select || [];
return mQueryOptions0.$select.every(mergeSelectPath);
default:
if (bAdd) {
mAggregatedQueryOptions[sName] = mQueryOptions0[sName];
return true;
}
return mQueryOptions0[sName] === mAggregatedQueryOptions[sName];
}
});
}
if (merge(mAggregatedQueryOptionsClone, mQueryOptions, sBaseMetaPath)) {
if (!bCacheImmutable) {
this.mAggregatedQueryOptions = mAggregatedQueryOptionsClone;
} else {
this.mLateQueryOptions = mAggregatedQueryOptionsClone;
}
return true;
}
return false;
};
/**
* Changes this binding's parameters and refreshes the binding. Since 1.111.0, a list binding's
* header context is deselected.
*
* 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 there are pending changes that cannot be ignored or if <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. Since 1.90.0, binding-specific parameters are ignored if they are unchanged. 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. 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.
* 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. Since 1.108.0,
* {@link sap.ui.model.odata.v4.Context#delete deleted} contexts do not count as pending
* changes.
*
* @public
* @since 1.45.0
*/
ODataParentBinding.prototype.changeParameters = function (mParameters) {
var mBindingParameters = Object.assign({}, this.mParameters),
sChangeReason, // @see sap.ui.model.ChangeReason
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 if (!sChangeReason) {
sChangeReason = ChangeReason.Change;
}
}
if (!mParameters) {
throw new Error("Missing map of binding parameters");
}
for (sKey in mParameters) {
if (sKey.startsWith("$$")) {
if (mParameters[sKey] === mBindingParameters[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);
}
};
/**
* Checks whether the given context may be kept alive.
*
* @param {sap.ui.model.odata.v4.Context} oContext
* A context of this binding
* @throws {Error} If <code>oContext.setKeepAlive()</code> is not allowed
*
* @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}
* A promise resolving without a defined result when the check is finished, or rejecting 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} 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}
* 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 () {
iCount -= 1;
if (iCount > 0) {
// Use a promise to get out of the prerendering loop
Promise.resolve().then(addUnlockTask);
} else if (that.oReadGroupLock === oGroupLock) {
// 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} 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}
* 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
*
* @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}
* 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
*/
ODataParentBinding.prototype.destroy = function () {
// this.mAggregatedQueryOptions = undefined;
this.aChildCanUseCachePromises = [];
this.removeReadGroupLock();
this.oResumePromise = undefined;
asODataBinding.prototype.destroy.call(this);
};
/**
* 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#fetchQueryOptionsForOwnCache}). 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} vChildQueryOptions
* The child binding's (aggregated) query options or a promise resolving with them
* @param {boolean} bIsProperty
* Whether the child is a property binding
* @returns {sap.ui.base.SyncPromise}
* 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 = _Helper.getMetaPath(oContext.getPath()),
bCacheImmutable,
oCanUseCachePromise,
// whether this binding is an operation or depends on one
bDependsOnOperation = oContext.getPath().includes("(...)"),
iIndex = oContext.getIndex(),
bIsAdvertisement = sChildPath[0] === "#",
oMetaModel = this.oModel.getMetaModel(),
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} 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 iIndex;
sPath = _Helper.getMetaPath(sPath);
iIndex = sPath.indexOf("@"); // Note: sPath[0] !== "@"
return iIndex > 0 ? sPath.slice(0, iIndex) : sPath;
}
if (bDependsOnOperation && !sResolvedChildPath.includes("/$Parameter/")
|| this.getRootBinding().isSuspended()
|| _Helper.isDataAggregation(this.mParameters)) {
// With data aggregation, no auto-$expand/$select is needed, but the child may still use
// the parent's cache
// Note: Operation bindings do not support auto-$expand/$select yet
return SyncPromise.resolve(sResolvedChildPath);
}
// 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.isKeepAlive() // kept-alive contexts have no index when not in aContexts
|| this.oCache === null
|| this.oCache && this.oCache.hasSentRequest();
aPromises = [
this.doFetchQueryOptions(this.oContext),
// 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
fetchPropertyAndType(),
vChildQueryOptions
];
oCanUseCachePromise = SyncPromise.all(aPromises).then(function (aResult) {
var mChildQueryOptions = aResult[2],
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.bHasPathReductionToParent = true;
return that.oContext.getBinding().fetchIfChildCanUseCache(that.oContext,
_Helper.getRelativePath(sResolvedChildPath, that.oContext.getPath()),
vChildQueryOptions);
}
if (bDependsOnOperation || sReducedChildMetaPath === "$count"
|| sReducedChildMetaPath.endsWith("/$count")) {
return sReducedPath;
}
if (that.bAggregatedQueryOptionsInitial) {
that.selectKeyProperties(mLocalQueryOptions, sBaseMetaPath);
that.mAggregatedQueryOptions = _Helper.clone(mLocalQueryOptions);
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 = _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) {
if (that.oCache) {
that.oCache.setLateQueryOptions(that.mLateQueryOptions);
} else if (that.oCache === null) {
return that.oContext.getBinding().fetchIfChildCanUseCache(that.oContext,
that.sPath, SyncPromise.resolve(that.mLateQueryOptions))
.then(function (sPath) {
return sPath && sReducedPath;
});
}
}
return sReducedPath;
});
this.aChildCanUseCachePromises.push(oCanUseCachePromise);
this.oCachePromise = SyncPromise.all([this.oCachePromise, oCanUseCachePromise])
.then(function (aResult) {
var oCache = aResult[0];
// Note: in operation bindings mAggregatedQueryOptions misses the options from
// $$inheritExpandSelect
// If the cache is immutable, only mLateQueryOptions may have changed
if (!bCacheImmutable && 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.mUriParameters,
that.mAggregatedQueryOptions));
}
}
return oCache;
});
// catch the error, but keep the rejected promise
this.oCachePromise.catch(function (oError) {
that.oModel.reportError(that + ": Failed to enhance query options for "
+ "auto-$expand/$select for child " + sChildPath, sClassName, oError);
});
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();
if (!(oModel.bAutoExpandSelect && mQueryOptions.$select)) {
return SyncPromise.resolve(mQueryOptions);
}
fnFetchMetadata = oModel.oInterface.fetchMetadata;
sMetaPath = _Helper.getMetaPath(oModel.resolve(this.sPath, oContext));
mConvertedQueryOptions = Object.assign({}, 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 () {
var mWrappedQueryOptions = _Helper.wrapChildQueryOptions(
sMetaPath, sSelectPath, {}, fnFetchMetadata);
if (mWrappedQueryOptions) {
_Helper.aggregateExpandSelect(mConvertedQueryOptions, mWrappedQueryOptions);
} else {
_Helper.addToSelect(mConvertedQueryOptions, [sSelectPath]);
}
});
})).then(function () {
return mConvertedQueryOptions;
});
};
/**
* 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 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 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 && this.oContext.getGeneration() || 0;
};
/**
* 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 (Object.keys(this.mParameters).length) {
// binding has parameters -> all query options need to be defined at the binding
return _Helper.getQueryOptionsForPath(this.getQueryOptionsFromParameters(), sPath);
}
oContext = 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#fetchQueryOptionsForOwnCache
* @see sap.ui.model.odata.v4.ODataBinding#doFetchQueryOptions
*/
/**
* @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,
bIgnoreInactiveCaches) {
return this.getDependentBindings().some(function (oDependent) {
var oCache = oDependent.oCache,
bHasPendingChanges,
bIgnoreKeptAlive = bIgnoreKeptAlive0; // new copy for this dependent only
if (bIgnoreKeptAlive) {
if (oDependent.oContext.isKeepAlive()) {
return false; // changes can be safely ignored here
}
if (oDependent.oContext.getIndex() !== 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 && !bIgnoreInactiveCaches) {
bHasPendingChanges = Object.keys(oDependent.mCacheByResourcePath)
.some(function (sPath) {
var oCacheForPath = oDependent.mCacheByResourcePath[sPath];
return oCacheForPath !== oCache // don't ask again
&& oCacheForPath.hasPendingChangesForPath("");
});
if (bHasPendingChanges) {
return true;
}
}
return oDependent.hasPendingChangesInDependents(bIgnoreKeptAlive,
bIgnoreInactiveCaches);
})
|| this.oModel.withUnresolvedBindings("hasPendingChangesInCaches",
this.getResolvedPath().slice(1));
};
/**
* 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
|| !this.isRoot() && this.oContext
&& this.oContext.getBinding().isPatchWithoutSideEffects();
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#isMeta
*/
ODataParentBinding.prototype.isMeta = function () {
return false;
};
/**
* Whether the dataRequested and dataReceived events related to the refresh must not be bubbled
* up to the model.
*
* @returns {boolean} Whether to prevent bubbling
*
* @private
* @see #createRefreshPromise
*/
ODataParentBinding.prototype.isRefreshWithoutBubbling = function () {
return this.oRefreshPromise && this.oRefreshPromise.$preventBubbling;
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#onDelete
*/
ODataParentBinding.prototype.onDelete = function (sCanonicalPath) {
var oContext = this.findContextForCanonicalPath(sCanonicalPath);
if (oContext) {
this.resetChangesForPath(this.getRelativePath(oContext.getPath()), []);
this.oModel.getDependentBindings(oContext).forEach(function (oBinding) {
oBinding.resetChanges();
});
this.delete(null, sCanonicalPath.slice(1), oContext);
}
};
/**
* Recursively refreshes all dependent list bindings that have no own cache.
*
* @returns {sap.ui.base.SyncPromise}
* A promise resolving when all dependent list bindings without own cache are refreshed; it is
* rejected when the refresh fails; the promise is resolved immediately on a suspended binding
* @throws {Error}
* If the binding's root binding is suspended and a group ID different from the binding's
* group ID is given
*
* @private
*/
ODataParentBinding.prototype.refreshDependentListBindingsWithoutCache = function () {
return SyncPromise.all(this.getDependentBindings().map(function (oDependentBinding) {
if (oDependentBinding.filter && oDependentBinding.oCache === null) {
return oDependentBinding.refreshInternal("");
}
if (oDependentBinding.refreshDependentListBindingsWithoutCache) {
return oDependentBinding.refreshDependentListBindingsWithoutCache();
}
}));
};
/**
* Unlocks a ReadGroupLock and removes it from the binding.
*
* @private
*/
ODataParentBinding.prototype.removeReadGroupLock = function () {
if (this.oReadGroupLock) {
this.oReadGroupLock.unlock(true);
this.oReadGroupLock = undefined;
}
};
/**
* Loads side effects for the given context of this binding.
*
* @param {string} sGroupId
* The group ID to be used for requesting side effects
* @param {string[]} aPaths
* The "14.5.11 Expression edm:NavigationPropertyPath" or
* "14.5.13 Expression edm:PropertyPath" strings describing which properties need to be loaded
* because they may have changed due to side effects of a previous update
* @param {sap.ui.model.odata.v4.Context} [oContext]
* The context for which to request side effects; if this parameter is missing or if it is the
* header context of a list binding, the whole binding is affected
* @returns {sap.ui.base.SyncPromise}
* A promise resolving without a defined result, or rejecting with an error if loading of side
* effects fails
* @throws {Error}
* If this binding does not use own data service requests or if the binding's root binding is
* suspended and the given group ID is not the binding's group
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding#requestSideEffects
* @private
* @see sap.ui.model.odata.v4.Context#requestSideEffects
*/
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#resetChangesInDependents
*/
ODataParentBinding.prototype.resetChangesInDependents = function (aPromises,
bIgnoreInactiveCaches) {
this.getDependentBindings().forEach(function (oDependent) {
aPromises.push(oDependent.oCachePromise.then(function (oCache) {
if (oCache) {
oCache.resetChangesForPath("");
}
oDependent.resetInvalidDataState();
}).unwrap());
// mCacheByResourcePath may have changes nevertheless
if (oDependent.mCacheByResourcePath && !bIgnoreInactiveCaches) {
Object.keys(oDependent.mCacheByResourcePath).forEach(function (sPath) {
oDependent.mCacheByResourcePath[sPath].resetChangesForPath("");
});
}
// Reset dependents, they might have no cache, but pending changes in
// mCacheByResourcePath
oDependent.resetChangesInDependents(aPromises, bIgnoreInactiveCaches);
});
};
/**
* Resumes this binding and all dependent bindings, fires a change or refresh event afterwards.
*
* @param {boolean} bCheckUpdate
* Parameter is ignored; dependent property bindings of a list binding never call checkUpdate
* @param {boolean} [bParentHasChanges]
* Whether there are changes on the parent binding that become active after resuming. If
* <code>true</code>, this binding is allowed to reuse the parent cache, otherwise this
* binding has to create its own cache
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataParentBinding#resumeInternal
* @private
*/
/**
* If there is a refresh promise created by {@link #createRefreshPromise}, it is resolved with
* the given promise and cleared. Does not reject the refresh promise with a canceled error.
*
* @param {Promise} oPromise - The promise to resolve with
* @returns {Promise} oPromise for chaining
*
* @private
*/
ODataParentBinding.prototype.resolveRefreshPromise = function (oPromise) {
if (this.oRefreshPromise) {
this.oRefreshPromise.$resolve(oPromise.catch(function (oError) {
if (!oError.canceled) {
throw oError;
}
}));
this.oRefreshPromise = null;
}
return oPromise;
};
/**
* Resumes this binding. The binding can again fire change events and trigger 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 (!thi