@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,357 lines (1,259 loc) • 52.6 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 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([
"./ODataBinding",
"./SubmitMode",
"./lib/_Helper",
"sap/base/Log",
"sap/ui/base/SyncPromise",
"sap/ui/model/ChangeReason"
], function (asODataBinding, SubmitMode, _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 = [];
// 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
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.attachPatchCompleted = function (fnFunction, oListener) {
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
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.detachPatchCompleted = function (fnFunction, oListener) {
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} <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
*/
/**
* 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
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.attachPatchSent = function (fnFunction, oListener) {
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
* @public
* @since 1.59.0
*/
ODataParentBinding.prototype.detachPatchSent = function (fnFunction, oListener) {
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 aggregated query options if necessary.
*
* The query options cannot be fulfilled if there are conflicts. A conflict is an option other
* than $expand, $select and $count which has different values in the aggregate and the options
* to be merged. This is checked recursively.
*
* Merging is not necessary if the binding's cache has already requested its data and the query
* options would extend $select. In this case the binding's cache will request the resp.
* property and add it when it is accessed.
*
* Note: * is an item in $select and $expand 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
* @returns {boolean} Whether the query options can be fulfilled by this binding
*
* @private
*/
ODataParentBinding.prototype.aggregateQueryOptions = function (mQueryOptions, sBaseMetaPath,
bCacheImmutable) {
var mAggregatedQueryOptionsClone
= _Helper.merge({}, this.mAggregatedQueryOptions, this.mLateQueryOptions),
bChanged = false,
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=false] Whether the given query options are inside a
* $expand
* @param {boolean} [bAdd=false] 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;
}
bChanged = true;
}
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.indexOf(sSelectPath) < 0) {
bChanged = true;
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).
return (!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 if (bChanged) {
this.mLateQueryOptions = mAggregatedQueryOptionsClone;
}
return true;
}
return false;
};
/**
* Changes this binding's parameters and refreshes the binding.
*
* If there are pending changes an error is thrown. Use {@link #hasPendingChanges} to check if
* there are pending changes. If there are changes, 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 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.
*
* @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;
function checkExpandSelect(sName) {
if (that.oModel.bAutoExpandSelect && sName in mParameters) {
throw new Error("Cannot change $expand or $select parameter in "
+ "auto-$expand/$select mode: "
+ sName + "=" + JSON.stringify(mParameters[sName]));
}
}
/*
* 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
*/
function updateChangeReason(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");
}
checkExpandSelect("$expand");
checkExpandSelect("$select");
if (this.hasPendingChanges()) {
throw new Error("Cannot change parameters due to pending changes");
}
for (sKey in mParameters) {
if (sKey.startsWith("$$")) {
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) {
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.ODataListBinding#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 (e.g. thrown by the change event handler of a control)
* @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.$resourcePath === 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 {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, fnErrorCallback, fnSubmitCallback) {
var that = this;
return this.oCachePromise.then(function (oCache) {
var sPathInCache;
if (oCache) {
sPathInCache = _Helper.getRelativePath(sCollectionPath,
that.oModel.resolve(that.sPath, that.oContext));
return oCache.create(oUpdateGroupLock, vCreatePath, sPathInCache,
sTransientPredicate, oInitialData, fnErrorCallback, fnSubmitCallback
).then(function (oCreatedEntity) {
if (oCache.$resourcePath) {
// 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.$resourcePath];
}
return oCreatedEntity;
});
}
return that.oContext.getBinding().createInCache(oUpdateGroupLock, vCreatePath,
sCollectionPath, sTransientPredicate, oInitialData, 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.
*
* @returns {Promise} the created promise
*
* @see #resolveRefreshPromise
* @private
*/
ODataParentBinding.prototype.createRefreshPromise = function () {
var oPromise, fnResolve;
oPromise = new Promise(function (resolve) {
fnResolve = resolve;
});
oPromise.$resolve = fnResolve;
this.oRefreshPromise = oPromise;
return oPromise;
};
/**
* 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; if no group ID is specified, it
* defaults to <code>getUpdateGroupId()</code>()
* @param {string} sEditUrl
* The edit URL to be used for the DELETE request
* @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.
* @param {function} fnCallback
* A function which is called after the entity has been deleted from the server and from the
* cache; the index of the entity is 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
* @throws {Error}
* If the group ID has {@link sap.ui.model.odata.v4.SubmitMode.Auto} or if the cache promise
* for this binding is not yet fulfilled
*
* @private
*/
ODataParentBinding.prototype.deleteFromCache = function (oGroupLock, sEditUrl, sPath,
oETagEntity, fnCallback) {
var sGroupId;
if (this.oCache === undefined) {
throw new Error("DELETE request not allowed");
}
if (this.oCache) {
sGroupId = oGroupLock.getGroupId();
if (!this.oModel.isAutoGroup(sGroupId) && !this.oModel.isDirectGroup(sGroupId)) {
throw new Error("Illegal update group ID: " + sGroupId);
}
return this.oCache._delete(oGroupLock, sEditUrl, sPath, oETagEntity, fnCallback);
}
return this.oContext.getBinding().deleteFromCache(oGroupLock, sEditUrl,
_Helper.buildPath(this.oContext.iIndex, this.sPath, sPath), oETagEntity, fnCallback);
};
/**
* 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
* The child binding's context, must not be <code>null</code> or <code>undefined</code>. See
* <code>sap.ui.model.odata.v4.ODataBinding#fetchQueryOptionsForOwnCache</code>.
* @param {string} sChildPath
* The child binding's binding path relative to <code>oContext</code>
* @param {sap.ui.base.SyncPromise} oChildQueryOptionsPromise
* Promise resolving with the child binding's (aggregated) query options
* @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; <code>undefined</code> otherwise.
*
* @private
* @see sap.ui.model.odata.v4.ODataMetaModel#getReducedPath
*/
ODataParentBinding.prototype.fetchIfChildCanUseCache = function (oContext, sChildPath,
oChildQueryOptionsPromise) {
// 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,
sFullMetaPath,
bIsAdvertisement = sChildPath[0] === "#",
oMetaModel = this.oModel.getMetaModel(),
aPromises,
sResolvedChildPath = this.oModel.resolve(sChildPath, oContext),
sResolvedPath = oContext.iReturnValueContextId
? oContext.getPath()
: this.oModel.resolve(this.sPath, this.oContext),
// whether this binding is an operation or depends on one
bDependsOnOperation = sResolvedPath.includes("(...)"),
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(sFullMetaPath.slice(0,
sFullMetaPath.lastIndexOf("/") + 1));
}
return _Helper.fetchPropertyAndType(that.oModel.oInterface.fetchMetadata,
sFullMetaPath);
}
if (bDependsOnOperation && !sResolvedChildPath.includes("/$Parameter/")
|| this.getRootBinding().isSuspended()
|| this.mParameters && this.mParameters.$$aggregation) {
// With $$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()
|| this.oCache === null
|| this.oCache && this.oCache.hasSentRequest();
sBaseMetaPath = oMetaModel.getMetaPath(oContext.getPath());
sFullMetaPath = oMetaModel.getMetaPath(sResolvedChildPath);
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(),
oChildQueryOptionsPromise
];
oCanUseCachePromise = SyncPromise.all(aPromises).then(function (aResult) {
var sChildMetaPath,
mChildQueryOptions = aResult[2],
mWrappedChildQueryOptions,
mLocalQueryOptions = aResult[0],
oProperty = aResult[1],
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);
if (sChildPath === "$count" || sChildPath.endsWith("/$count")
|| sChildPath[0] === "@") {
return SyncPromise.resolve(sReducedPath);
}
if (_Helper.getRelativePath(sReducedPath, sResolvedPath) === undefined) {
return that.oContext.getBinding().fetchIfChildCanUseCache(that.oContext,
_Helper.getRelativePath(sResolvedChildPath, that.oContext.getPath()),
oChildQueryOptionsPromise);
}
if (bDependsOnOperation) {
return SyncPromise.resolve(sReducedPath);
}
sChildMetaPath = _Helper.getRelativePath(_Helper.getMetaPath(sReducedPath),
sBaseMetaPath);
if (that.bAggregatedQueryOptionsInitial) {
that.selectKeyProperties(mLocalQueryOptions, sBaseMetaPath);
that.mAggregatedQueryOptions = _Helper.clone(mLocalQueryOptions);
that.bAggregatedQueryOptionsInitial = false;
}
if (bIsAdvertisement) {
mWrappedChildQueryOptions = {"$select" : [sChildMetaPath.slice(1)]};
return that.aggregateQueryOptions(mWrappedChildQueryOptions, sBaseMetaPath,
bCacheImmutable)
? sReducedPath
: undefined;
}
if (sChildMetaPath === ""
|| oProperty
&& (oProperty.$kind === "Property" || oProperty.$kind === "NavigationProperty")) {
mWrappedChildQueryOptions = _Helper.wrapChildQueryOptions(sBaseMetaPath,
sChildMetaPath, mChildQueryOptions, that.oModel.oInterface.fetchMetadata);
if (mWrappedChildQueryOptions) {
return that.aggregateQueryOptions(mWrappedChildQueryOptions, sBaseMetaPath,
bCacheImmutable)
? sReducedPath
: undefined;
}
return undefined;
}
if (sChildMetaPath === "value") { // symbolic name for operation result
return that.aggregateQueryOptions(mChildQueryOptions, sBaseMetaPath,
bCacheImmutable)
? sReducedPath
: undefined;
}
Log.error("Failed to enhance query options for auto-$expand/$select as the path '"
+ sFullMetaPath + "' 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 {
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 (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) {
return _Helper.fetchPropertyAndType(
fnFetchMetadata, sMetaPath + "/" + sSelectPath
).then(function () {
var mWrappedQueryOptions = _Helper.wrapChildQueryOptions(
sMetaPath, sSelectPath, {}, fnFetchMetadata);
if (mWrappedQueryOptions) {
_Helper.aggregateQueryOptions(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.getGroupProperty(sParentUpdateGroupId, "submit")
!== SubmitMode.API) {
return oParentBinding.getBaseForPathReduction();
}
}
return this.oModel.resolve(this.sPath, this.oContext);
};
/**
* Returns the query options used in the cache.
*
* @returns {object} The query options
*
* @private
*/
ODataParentBinding.prototype.getCacheQueryOptions = function () {
return this.mCacheQueryOptions
|| _Helper.getQueryOptionsForPath(
this.oContext.getBinding().getCacheQueryOptions(), 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 (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 () {
var aDependents = this.getDependentBindings();
return aDependents.some(function (oDependent) {
var oCache = oDependent.oCache,
bHasPendingChanges;
if (oCache !== undefined) {
// Pending changes for this cache are only possible when there is a cache already
if (oCache && oCache.hasPendingChangesForPath("")) {
return true;
}
} else if (oDependent.hasPendingChangesForPath("")) {
return true;
}
if (oDependent.mCacheByResourcePath) {
bHasPendingChanges = Object.keys(oDependent.mCacheByResourcePath)
.some(function (sPath) {
return oDependent.mCacheByResourcePath[sPath].hasPendingChangesForPath("");
});
if (bHasPendingChanges) {
return true;
}
}
// Ask dependents, they might have no cache, but pending changes in mCacheByResourcePath
return oDependent.hasPendingChangesInDependents();
})
|| this.oModel.withUnresolvedBindings("hasPendingChangesInCaches",
this.oModel.resolve(this.sPath, this.oContext).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;
};
/**
* Refreshes all dependent bindings with the given parameters and waits for them to have
* finished.
*
* @param {string} sResourcePathPrefix
* The resource path prefix which is used to delete the dependent caches and corresponding
* messages; may be "" but not <code>undefined</code>
* @param {string} [sGroupId]
* The group ID to be used for refresh
* @param {boolean} [bCheckUpdate]
* If <code>true</code>, a property binding is expected to check for updates
* @param {boolean} [bKeepCacheOnError]
* If <code>true</code>, the binding data remains unchanged if the refresh fails
* @returns {sap.ui.base.SyncPromise}
* A promise resolving when all dependent bindings are refreshed; it is rejected if the
* binding's root binding is suspended and a group ID different from the binding's group ID is
* given
*
* @private
*/
ODataParentBinding.prototype.refreshDependentBindings = function (sResourcePathPrefix, sGroupId,
bCheckUpdate, bKeepCacheOnError) {
return SyncPromise.all(this.getDependentBindings().map(function (oDependentBinding) {
return oDependentBinding.refreshInternal(sResourcePathPrefix, sGroupId, bCheckUpdate,
bKeepCacheOnError);
}));
};
/**
* 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
*
* @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;
}
};
/**
* Refreshes the binding; expects it to be suspended.
*
* @param {string} sGroupId
* The group ID to be used for the refresh
* @throws {Error}
* If a group ID different from the binding's group ID is given
* @private
*/
ODataParentBinding.prototype.refreshSuspended = function (sGroupId) {
if (sGroupId && sGroupId !== this.getGroupId()) {
throw new Error(this + ": Cannot refresh a suspended binding with group ID '"
+ sGroupId + "' (own group ID is '" + this.getGroupId() + "')");
}
this.setResumeChangeReason(ChangeReason.Refresh);
};
/**
* Requests side effects for the given absolute paths.
*
* @param {string} sGroupId
* The effective group ID
* @param {string[]} aAbsolutePaths
* The absolute paths to request side effects for
* @returns {sap.ui.base.SyncPromise}
* A promise resolving without a defined result, or rejecting with an error if loading of side
* effects fails, or <code>undefined</code> if there is nothing to do
*
* @private
*/
ODataParentBinding.prototype.requestAbsoluteSideEffects = function (sGroupId, aAbsolutePaths) {
var aPaths = [],
sMetaPath = _Helper.getMetaPath(this.oModel.resolve(this.sPath, this.oContext)),
bRefresh;
bRefresh = aAbsolutePaths.some(function (sAbsolutePath) {
var sRelativePath = _Helper.getRelativePath(sAbsolutePath, sMetaPath);
if (sRelativePath !== undefined) {
aPaths.push(sRelativePath);
}
return _Helper.hasPathPrefix(sMetaPath, sAbsolutePath);
});
if (bRefresh) {
return this.refreshInternal("", sGroupId);
} else if (aPaths.length) {
return this.requestSideEffects(sGroupId, aPaths);
}
// return 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) {
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) {
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);
});
};
/**
* 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
*/
/**
* Resolves and clears the refresh promise created by {@link #createRefreshPromise} with the
* given result if there is one.
*
* @param {any} vResult - The result to resolve with
* @returns {any} vResult for chaining
*
* @private
*/
ODataParentBinding.prototype.resolveRefreshPromise = function (vResult) {
if (this.oRefreshPromise) {
this.oRefreshPromise.$resolve(vResult);
this.oRefreshPromise = null;
}
return vResult;
};
/**
* 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 a {@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();
}
};
/**
* Resumes this binding. The binding can then again fire change events and trigger data service
* requests.
* Before 1.53.0, this method was not supported and threw an error.
*
* @throws {Error}
* If this binding is relative to a {@link sap.ui.model.odata.v4.Context} or if it is an
* operation binding or if it is not suspended
*
* @public
* @see sap.ui.model.Binding#resume
* @see #suspend
* @since 1.37.0
*/
// @override sap.ui.model.Binding#resume
ODataParentBinding.prototype.resume = function () {
this._resume(false);
};
/**
* Resumes this binding asynchronously as soon as the next rendering starts.
*
* Note: Some API calls are not allowed as long as the binding is in suspended mode, hence the
* returned promise can be used to get the point in time when these APIs can be called again.
*
* @returns {Promise} A promise which is resolved when the binding is resumed.
* @throws {Error}
* If this binding is relative to a {@link sap.ui.model.odata.v4.Context} or if it is an
* operation binding or if it is not suspended
*
* @protected
* @see #resume
* @see #suspend
* @since 1.75.0
*/
ODataParentBinding.prototype.resumeAsync = function () {
this._resume(true);
return Promise.resolve(this.oResumePromise);
};
/**
* Adds the key properties of the entity reached by the given navigation property path to
* $select of the query options. Expects that the type has already been loaded so that it can
* be accessed synchronously.
*
* @param {object} mQueryOptions The query options
* @param {string} sMetaPath The path to the navigation property
*
* @private
*/
ODataParentBinding.prototype.selectKeyProperties = function (mQueryOptions, sMetaPath) {
_Helper.selectKeyProperties(mQueryOptions,
this.oModel.getMetaModel().getObject(sMetaPath + "/"));
};
/**
* Suspends this binding. A suspended binding does not fire change events nor does it trigger
* data service requests. Call {@link #resume} to resume the binding.
* Before 1.53.0, this method was not supported and threw an error.
*
* @throws {Error}
* If this binding is relative to a {@link sap.ui.model.odata.v4.Context} or if it is an
* operation binding or if it is already suspended or if it has pending changes
*
* @public
* @see sap.ui.model.Binding#suspend
* @see sap.ui.model.odata.v4.ODataContextBinding#hasPendingChanges
* @see sap.ui.model.odata.v4.ODataListBinding#hasPendingChanges
* @see #resume
* @since 1.37.0
*/
// @override sap.ui.model.Binding#suspend
ODataParentBinding.prototype.suspend = function () {
var fnResolve;
if (this.oOperation) {
throw new Error("Cannot suspend an operation binding: " + this);
}
if (!this.isRoot()) {
throw new Error("Cannot suspend a relative binding: " + this);
}
if (this.bSuspended) {
throw new Error("Cannot suspend a suspended binding: " + this);
}
if (this.hasPendingChanges()) {
throw new Error("Cannot suspend a binding with pending changes: " + this);
}
this.bSuspended = true;
this.oResumePromise = new SyncPromise(function (resolve) {
fnResolve = resolve;
});
this.oResumePromise.$resolve = fnResolve;
this.removeReadGroupLock();
};
/**
* Updates the aggregated query options of this binding with the values from the given
* query options. "$select" and "$expand" are only updated if the aggregated query options are
* still initial because these have been computed in {@link #fetchIfChildCanUseCache} otherwise.
* Note: If the aggregated query options contain a key which is not contained in the given
* query options, it is deleted from the aggregated query options.
*
* @param {object} mNewQueryOptions
* The query options to update the aggregated query options
*
* @private
*/
ODataParentBinding.prototype.updateAggregatedQueryOptions = function (mNewQueryOptions) {
var aAllKeys = Object.keys(mNewQ