@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,259 lines (1,169 loc) • 75.9 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
//Provides class sap.ui.model.odata.v4.ODataContextBinding
sap.ui.define([
"./Context",
"./ODataParentBinding",
"./lib/_Cache",
"./lib/_GroupLock",
"./lib/_Helper",
"sap/ui/base/SyncPromise",
"sap/ui/model/Binding",
"sap/ui/model/ChangeReason",
"sap/ui/model/ContextBinding"
], function (Context, asODataParentBinding, _Cache, _GroupLock, _Helper, SyncPromise, Binding,
ChangeReason, ContextBinding) {
"use strict";
var sClassName = "sap.ui.model.odata.v4.ODataContextBinding",
mSupportedEvents = {
AggregatedDataStateChange : true,
change : true,
dataReceived : true,
dataRequested : true,
DataStateChange : true,
patchCompleted : true,
patchSent : true
},
/**
* @alias sap.ui.model.odata.v4.ODataContextBinding
* @author SAP SE
* @class Context binding for an OData V4 model.
* An event handler can only be attached to this binding for the following events:
* 'AggregatedDataStateChange', 'change', 'dataReceived', 'dataRequested',
* 'DataStateChange', 'patchCompleted', and 'patchSent'. For other events, an error is
* thrown.
*
* A context binding can also be used as an <i>operation binding</i> to support bound
* actions, action imports, bound functions and function imports. If you want to control
* the invocation time of an operation, for example a function import named
* "GetNumberOfAvailableItems", create a context binding for the path
* "/GetNumberOfAvailableItems(...)" (as specified here, including the three dots). Such
* an operation binding is <i>deferred</i>, meaning that it does not request
* automatically, but only when you call {@link #invoke}. {@link #refresh} is always
* ignored for actions and action imports. For bound functions and function imports, it is
* ignored if {@link #invoke} has not yet been called. Afterwards it results in another
* call of the function with the parameter values of the last invocation.
*
* The binding parameter for bound actions or bound functions may be given in the binding
* path, for example "/SalesOrderList('42')/name.space.SalesOrder_Confirm". This can be
* used if the exact entity for the binding parameter is known in advance. If you use a
* relative binding instead, the operation path is a concatenation of the parent context's
* canonical path and the deferred binding's path.
*
* <b>Example</b>: You have a table with a list binding to "/SalesOrderList". In
* each row you have a button to confirm the sales order, with the relative binding
* "name.space.SalesOrder_Confirm(...)". Then the parent context for such a button
* refers to an entity in "SalesOrderList", so its canonical path is
* "/SalesOrderList('<i>SalesOrderID</i>')" and the resulting path for the action
* is "/SalesOrderList('<i>SalesOrderID</i>')/name.space.SalesOrder_Confirm".
*
* This also works if the relative path of the deferred operation binding starts with a
* navigation property. Then this navigation property will be part of the operation's
* resource path, which is still valid.
*
* A deferred operation binding is not allowed to have another deferred operation binding
* as parent.
*
* @extends sap.ui.model.ContextBinding
* @hideconstructor
* @mixes sap.ui.model.odata.v4.ODataParentBinding
* @public
* @since 1.37.0
* @version 1.146.0
*
* @borrows sap.ui.model.odata.v4.ODataBinding#getGroupId as #getGroupId
* @borrows sap.ui.model.odata.v4.ODataBinding#getRootBinding as #getRootBinding
* @borrows sap.ui.model.odata.v4.ODataBinding#getUpdateGroupId as #getUpdateGroupId
* @borrows sap.ui.model.odata.v4.ODataBinding#hasPendingChanges as #hasPendingChanges
* @borrows sap.ui.model.odata.v4.ODataBinding#isInitial as #isInitial
* @borrows sap.ui.model.odata.v4.ODataBinding#refresh as #refresh
* @borrows sap.ui.model.odata.v4.ODataBinding#requestRefresh as #requestRefresh
* @borrows sap.ui.model.odata.v4.ODataBinding#resetChanges as #resetChanges
* @borrows sap.ui.model.odata.v4.ODataBinding#toString as #toString
* @borrows sap.ui.model.odata.v4.ODataParentBinding#attachPatchCompleted as
* #attachPatchCompleted
* @borrows sap.ui.model.odata.v4.ODataParentBinding#attachPatchSent as #attachPatchSent
* @borrows sap.ui.model.odata.v4.ODataParentBinding#changeParameters as #changeParameters
* @borrows sap.ui.model.odata.v4.ODataParentBinding#detachPatchCompleted as
* #detachPatchCompleted
* @borrows sap.ui.model.odata.v4.ODataParentBinding#detachPatchSent as #detachPatchSent
* @borrows sap.ui.model.odata.v4.ODataParentBinding#resume as #resume
* @borrows sap.ui.model.odata.v4.ODataParentBinding#suspend as #suspend
*/
ODataContextBinding = ContextBinding.extend("sap.ui.model.odata.v4.ODataContextBinding", {
constructor : constructor
});
//*********************************************************************************************
// ODataContextBinding
//*********************************************************************************************
/**
* Do <strong>NOT</strong> call this private constructor, but rather use
* {@link sap.ui.model.odata.v4.ODataModel#bindContext} instead!
*
* @param {sap.ui.model.odata.v4.ODataModel} oModel
* The OData V4 model
* @param {string} sPath
* The binding path in the model; must not end with a slash
* @param {sap.ui.model.Context} [oContext]
* The context which is required as base for a relative path
* @param {object} [mParameters]
* Map of binding parameters
* @throws {Error}
* If disallowed binding parameters are provided
*/
function constructor(oModel, sPath, oContext, mParameters) {
var iPos = sPath.indexOf("(...)"),
that = this;
ContextBinding.call(this, oModel, sPath);
// initialize mixin members
asODataParentBinding.call(this);
if (sPath.endsWith("/")) {
throw new Error("Invalid path: " + sPath);
}
// Whether the binding has fetched its own $select/$expand in the current parent cache
this.bHasFetchedExpandSelectProperties = false;
this.oOperation = undefined;
this.oParameterContext = null;
this.oReturnValueContext = null;
if (iPos >= 0) { // deferred operation binding
if (iPos !== this.sPath.length - /*"(...)".length*/5) {
throw new Error(
"The path must not continue after a deferred operation: " + this.sPath);
}
this.oOperation = {
bAction : undefined,
// Whether the operation is a bound action with a navigation property inside the
// path to its binding parameter, and thus additional ($expand/$select) query
// options have been computed to facilitate a RVC.
// undefined: unknown whether it is possible to determine the needed query options
// true: query options are determined
// false: the preconditions are not given to determine the query options
bAdditionalQueryOptionsForRVC : undefined,
mChangeListeners : {}, // map from path to an array of change listeners
mParameters : {},
mRefreshParameters : {}
};
if (!this.bRelative) {
this.oParameterContext = Context.create(this.oModel, this,
this.sPath + "/$Parameter");
}
}
mParameters = _Helper.clone(mParameters) || {};
// Note: needs this.oOperation
this.checkBindingParameters(mParameters, ["$$canonicalPath", "$$groupId",
"$$inheritExpandSelect", "$$ownRequest", "$$patchWithoutSideEffects",
"$$updateGroupId"]);
this.sGroupId = mParameters.$$groupId;
this.bInheritExpandSelect = mParameters.$$inheritExpandSelect;
this.sUpdateGroupId = mParameters.$$updateGroupId;
this.applyParameters(mParameters);
this.oElementContext = this.bRelative
? null
: Context.createNewContext(this.oModel, this, sPath);
if (!this.oOperation
&& (!this.bRelative || oContext && !oContext.fetchValue)) { // @see #isRoot
// do this before #setContext fires an event!
this.createReadGroupLock(this.getGroupId(), true);
}
this.setContext(oContext);
oModel.bindingCreated(this);
Promise.resolve().then(function () {
// bInitial must be true initially, but false later. Then suspend on a just
// created binding causes a change event on resume; otherwise further changes
// on the suspended binding are required (see doSuspend)
that.bInitial = false;
});
}
asODataParentBinding(ODataContextBinding.prototype);
/**
* Calls the OData operation that corresponds to this operation binding.
*
* @param {sap.ui.model.odata.v4.lib._GroupLock} oGroupLock
* A lock for the group ID to be used for the request
* @param {map} mParameters
* The parameter map at the time of the invocation
* @param {boolean} [bIgnoreETag]
* Whether the entity's ETag should be actively ignored (If-Match:*); supported for bound
* actions only
* @param {function} [fnOnStrictHandlingFailed]
* Callback for strict handling; supported for actions only
* @param {boolean} [bReplaceWithRVC]
* Whether this operation binding's parent context, which must belong to a list binding, is
* replaced with the operation's return value context (see below) and that new list context is
* returned instead. Since 1.97.0.
* @returns {Promise<sap.ui.model.odata.v4.Context|undefined>}
* A promise that is resolved without data or with a return value context when the operation
* call succeeded, or rejected with an <code>Error</code> instance <code>oError</code> in case
* of failure.
*
* @private
* @see #invoke for details
*/
ODataContextBinding.prototype._invoke = function (oGroupLock, mParameters, bIgnoreETag,
fnOnStrictHandlingFailed, bReplaceWithRVC) {
var oMetaModel = this.oModel.getMetaModel(),
oOperationMetadata,
oPromise,
sResolvedPath = this.getResolvedPathWithReplacedTransientPredicates(),
sResolvedMetaPath = _Helper.getMetaPath(sResolvedPath),
that = this;
/*
* Fires a "change" event and refreshes dependent bindings.
* @returns {sap.ui.base.SyncPromise<void>} A promise resolving when the refresh is finished
*/
function fireChangeAndRefreshDependentBindings() {
that._fireChange({reason : ChangeReason.Change});
return that.refreshDependentBindings("", oGroupLock.getGroupId(), true);
}
oPromise = SyncPromise.all([
oMetaModel.fetchObject(sResolvedMetaPath + "/@$ui5.overload"),
this.ready2Inherit()
]).then(function ([aOperationMetadata]) {
var fnGetEntity, iIndex, sPath;
if (!aOperationMetadata) {
oOperationMetadata = oMetaModel.getObject(sResolvedMetaPath);
if (!oOperationMetadata || oOperationMetadata.$kind !== "NavigationProperty"
|| !bReplaceWithRVC) {
throw new Error("Unknown operation: " + sResolvedPath);
}
} else if (aOperationMetadata.length !== 1) {
throw new Error("Expected a single overload, but found "
+ aOperationMetadata.length + " for " + sResolvedPath);
} else {
oOperationMetadata = aOperationMetadata[0];
}
if (that.bRelative && that.oContext.getBinding) {
iIndex = that.sPath.lastIndexOf("/");
sPath = iIndex >= 0 ? that.sPath.slice(0, iIndex) : "";
fnGetEntity = that.oContext.getValue.bind(that.oContext, sPath);
}
return that.createCacheAndRequest(oGroupLock, sResolvedPath, oOperationMetadata,
mParameters, fnGetEntity, bIgnoreETag, fnOnStrictHandlingFailed);
}).then(function (oResponseEntity) {
return fireChangeAndRefreshDependentBindings().then(function () {
return that.handleOperationResult(oOperationMetadata,
oResponseEntity, bReplaceWithRVC);
});
}, function (oError) {
// Note: operation metadata is only needed to handle server messages, it is
// available if oError.error exists! If not nothing to do here.
_Helper.adjustTargetsInError(oError, oOperationMetadata,
that.oParameterContext.getPath(),
that.bRelative ? that.oContext.getPath() : undefined);
// Note: this must be done after the targets have been normalized, because otherwise
// a child reports the messages from the error response with wrong targets
return fireChangeAndRefreshDependentBindings().then(function () {
throw oError;
});
}).catch(function (oError) {
oGroupLock.unlock(true);
that.oModel.reportError("Failed to invoke " + sResolvedPath, sClassName, oError);
throw oError;
});
return Promise.resolve(oPromise);
};
/**
* Adds the query options for determining the key properties of a return value context.
* If all preconditions are fulfilled (see {@link #isReturnValueLikeBindingParameter} and
* {@link #hasReturnValueContext}) and it was possible to determine the query options, the flag
* <code>bAdditionalQueryOptionsForRVC</code> in <code>this.oOperation</code> is set to
* <code>true</code>, if it was not possible the flag is set to <code>false</code>.
*
* @param {object} oOperationMetadata
* The operation's metadata
* @param {object} mQueryOptions
* The operation binding's cache query options
* @returns {object}
* The computed query options
*
* @private
*/
ODataContextBinding.prototype.addQueryOptionsForReturnValueContext
= function (oOperationMetadata, mQueryOptions) {
const aMetaSegments = _Helper.getMetaPath(this.getResolvedPath()).split("/");
if (!this.isReturnValueLikeBindingParameter(oOperationMetadata)
|| !this.hasReturnValueContext() || aMetaSegments.length !== 4) {
this.oOperation.bAdditionalQueryOptionsForRVC = false;
return mQueryOptions;
}
const oMetaModel = this.oModel.getMetaModel();
const sEntitySet = oMetaModel.getObject(
"/" + aMetaSegments[1] + "/$NavigationPropertyBinding/" + aMetaSegments[2]);
const sPartner = oMetaModel.getObject(
"/" + aMetaSegments[1] + "/" + aMetaSegments[2] + "/$Partner");
let mAdditionalQueryOptions;
if (sEntitySet && sPartner) {
mAdditionalQueryOptions = {$expand : {}};
mAdditionalQueryOptions.$expand[sPartner] = {};
_Helper.selectKeyProperties(mAdditionalQueryOptions.$expand[sPartner],
oMetaModel.getObject("/" + sEntitySet + "/" + sPartner + "/"));
if (mQueryOptions.$select) {
_Helper.selectKeyProperties(mAdditionalQueryOptions,
oMetaModel.getObject("/" + sEntitySet + "/"));
}
mQueryOptions = _Helper.clone(mQueryOptions); // "copy on write"
_Helper.aggregateExpandSelect(mQueryOptions, mAdditionalQueryOptions);
}
this.oOperation.bAdditionalQueryOptionsForRVC = !!mAdditionalQueryOptions;
return mQueryOptions;
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#adjustPredicate
*/
ODataContextBinding.prototype.adjustPredicate = function (sTransientPredicate, sPredicate) {
asODataParentBinding.prototype.adjustPredicate.apply(this, arguments);
if (this.mCacheQueryOptions) {
// There are mCacheQueryOptions, but #prepareDeepCreate prevented creating the cache
this.fetchCache(this.oContext, true);
}
if (this.oElementContext) {
this.oElementContext.adjustPredicate(sTransientPredicate, sPredicate);
}
// this.oReturnValueContext cannot have the transient predicate; it results from #invoke
// which is not possible with a transient predicate
};
/**
* Applies the given map of parameters to this binding's parameters.
*
* @param {object} mParameters
* Map of binding parameters, {@link sap.ui.model.odata.v4.ODataModel#constructor}
* @param {sap.ui.model.ChangeReason} [sChangeReason]
* A change reason (either <code>undefined</code> or <code>ChangeReason.Change</code>), only
* used to distinguish calls by {@link #constructor} from calls by
* {@link sap.ui.model.odata.v4.ODataParentBinding#changeParameters}
*
* @private
*/
ODataContextBinding.prototype.applyParameters = function (mParameters, sChangeReason) {
this.mQueryOptions = this.oModel.buildQueryOptions(mParameters, true);
this.mParameters = mParameters; // store mParameters at binding after validation
if (this.isRootBindingSuspended()) {
if (!this.oOperation) {
this.sResumeChangeReason = ChangeReason.Change;
}
} else if (!this.oOperation) {
this.fetchCache(this.oContext);
if (sChangeReason) {
this.refreshInternal("", undefined, true).catch(this.oModel.getReporter());
}
} else if (this.oOperation.bAction === false) {
this.invoke().catch(this.oModel.getReporter());
}
};
/**
* The 'change' event is fired when the binding is initialized or its parent context is changed.
* It is to be used by controls to get notified about changes to the bound context of this
* context binding.
* Registered event handlers are called with the change reason as parameter.
*
* @param {sap.ui.base.Event} oEvent
* The event object
* @param {function():Object<any>} oEvent.getParameters
* Function which returns an object containing all event parameters
* @param {sap.ui.model.ChangeReason} oEvent.getParameters.reason
* The reason for the 'change' event could be
* <ul>
* <li> {@link sap.ui.model.ChangeReason.Change Change} when the binding is initialized,
* when an operation has been processed (see {@link #invoke}), or in {@link #resume} when
* the binding has been modified while suspended,
* <li> {@link sap.ui.model.ChangeReason.Refresh Refresh} when the binding is refreshed,
* <li> {@link sap.ui.model.ChangeReason.Context Context} when the parent context is
* changed,
* <li> {@link sap.ui.model.ChangeReason.Remove Remove} when the element context has been
* deleted (see {@link sap.ui.model.odata.v4.Context#delete}).
* </ul>
*
* @event sap.ui.model.odata.v4.ODataContextBinding#change
* @public
* @since 1.37.0
*/
/**
* The 'dataReceived' event is fired after the back-end data has been processed. It is only
* fired for GET requests. The 'dataReceived' event is to be used by applications, for example
* to switch off a busy indicator or to process an error. In case of a deferred operation
* binding, 'dataReceived' is not fired: Whatever should happen in the event handler attached
* to that event, can instead be done once the <code>oPromise</code> returned by
* {@link #invoke} fulfills or rejects (using <code>oPromise.then(function () {...}, function
* () {...})</code>).
*
* If back-end requests are successful, the event has almost no parameters. For compatibility
* with {@link sap.ui.model.Binding#event:dataReceived 'dataReceived'}, an event parameter
* <code>data : {}</code> is provided: "In error cases it will be undefined", but otherwise it
* is not. Use the binding's bound context via
* {@link #getBoundContext oEvent.getSource().getBoundContext()} to access the response data.
* Note that controls bound to this data may not yet have been updated, meaning it is not safe
* for registered event handlers to access data via control APIs.
*
* If a back-end request fails, the 'dataReceived' event provides an <code>Error</code> in the
* 'error' event parameter.
*
* Since 1.106, this event is bubbled up to the model, unless a listener calls
* {@link sap.ui.base.Event#cancelBubble oEvent.cancelBubble()}.
*
* @param {sap.ui.base.Event} oEvent
* The event object
* @param {function} oEvent.cancelBubble
* A callback function to prevent that the event is bubbled up to the model
* @param {function():Object<any>} oEvent.getParameters
* Function which returns an object containing all event parameters
* @param {object} [oEvent.getParameters.data]
* An empty data object if a back-end request succeeds
* @param {Error} [oEvent.getParameters.error] The error object if a back-end request failed.
* If there are multiple failed back-end requests, the error of the first one is provided.
*
* @event sap.ui.model.odata.v4.ODataContextBinding#dataReceived
* @public
* @see sap.ui.model.odata.v4.ODataModel#event:dataReceived
* @since 1.37.0
*/
/**
* The 'dataRequested' event is fired directly after data has been requested from a back end.
* It is only fired for GET requests. The 'dataRequested' event is to be used by
* applications, for example to switch on a busy indicator. Registered event handlers are
* called without parameters. In case of a deferred operation binding, 'dataRequested' is not
* fired: Whatever should happen in the event handler attached to that event, can instead be
* done before calling {@link #invoke}.
*
* Since 1.106, this event is bubbled up to the model, unless a listener calls
* {@link sap.ui.base.Event#cancelBubble oEvent.cancelBubble()}.
*
* @param {sap.ui.base.Event} oEvent
* @param {function} oEvent.cancelBubble
* A callback function to prevent that the event is bubbled up to the model
*
* @event sap.ui.model.odata.v4.ODataContextBinding#dataRequested
* @public
* @see sap.ui.model.odata.v4.ODataModel#event:dataRequested
* @since 1.37.0
*/
/**
* The 'patchCompleted' event is fired when the back end has responded to the last PATCH
* request for this binding. If there is more than one PATCH request in a $batch, the event is
* fired only once. Only bindings using an own data service request fire a 'patchCompleted'
* event. For each 'patchSent' event, a 'patchCompleted' event is fired.
*
* @param {sap.ui.base.Event} oEvent The event object
* @param {sap.ui.model.odata.v4.ODataContextBinding} oEvent.getSource() This binding
* @param {function():Object<any>} oEvent.getParameters
* Function which returns an object containing all event parameters
* @param {boolean} oEvent.getParameters.success
* Whether all PATCHes are successfully processed
*
* @event sap.ui.model.odata.v4.ODataContextBinding#patchCompleted
* @public
* @since 1.59.0
*/
/**
* The 'patchSent' event is fired when the first PATCH request for this binding is sent to the
* back end. If there is more than one PATCH request in a $batch, the event is fired only once.
* Only bindings using an own data service request fire a 'patchSent' event. For each
* 'patchSent' event, a 'patchCompleted' event is fired.
*
* @param {sap.ui.base.Event} oEvent The event object
* @param {sap.ui.model.odata.v4.ODataContextBinding} oEvent.getSource() This binding
*
* @event sap.ui.model.odata.v4.ODataContextBinding#patchSent
* @public
* @since 1.59.0
*/
/**
* See {@link sap.ui.base.EventProvider#attachEvent}
*
* @param {string} sEventId The identifier of the event to listen for
* @param {object} [_oData]
* @param {function} [_fnFunction]
* @param {object} [_oListener]
* @returns {this} <code>this</code> to allow method chaining
*
* @public
* @see sap.ui.base.EventProvider#attachEvent
* @since 1.37.0
*/
// @override sap.ui.base.EventProvider#attachEvent
ODataContextBinding.prototype.attachEvent = function (sEventId, _oData, _fnFunction,
_oListener) {
if (!(sEventId in mSupportedEvents)) {
throw new Error("Unsupported event '" + sEventId
+ "': v4.ODataContextBinding#attachEvent");
}
return ContextBinding.prototype.attachEvent.apply(this, arguments);
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataParentBinding#checkKeepAlive
*/
ODataContextBinding.prototype.checkKeepAlive = function () {
throw new Error("Unsupported " + this);
};
/**
* Returns this operation binding's cache query options.
*
* @returns {object} The query options
*
* @private
*/
ODataContextBinding.prototype.computeOperationQueryOptions = function () {
return Object.assign({}, this.oModel.mURLParameters, this.getQueryOptionsFromParameters());
};
/**
* Creates a single cache for an operation and sends a GET/POST request.
*
* @param {sap.ui.model.odata.v4.lib._GroupLock} oGroupLock
* A lock for the group ID to be used for the request
* @param {string} sPath
* The absolute binding path to the bound operation or operation import, e.g.
* "/Entity('0815')/bound.Operation(...)" or "/OperationImport(...)"
* @param {object} oOperationMetadata
* The operation's metadata
* @param {map} mParameters
* The parameter map at the time of the invocation
* @param {function} [fnGetEntity]
* An optional function which may be called to access the existing entity data (if already
* loaded) in case of a bound operation
* @param {boolean} [bIgnoreETag]
* Whether the entity's ETag should be actively ignored (If-Match:*); supported for bound
* actions only
* @param {function} [fnOnStrictHandlingFailed]
* Callback for strict handling; supported for actions only
* @returns {sap.ui.base.SyncPromise<any>}
* The request promise
* @throws {Error} If
* <ul>
* <li> the given metadata is neither an "Action" nor a "Function" nor a
* "NavigationProperty",
* <li> a collection-valued parameter for an operation other than a V4 action is encountered,
* <li> <code>bIgnoreETag</code> is used for an operation other than a bound action,
* <li> <code>fnOnStrictHandlingFailed</code> is given but the given metadata is not an
* "Action",
* <li> a navigation property is used with operation parameters
* </ul>
*
* @private
*/
ODataContextBinding.prototype.createCacheAndRequest = function (oGroupLock, sPath,
oOperationMetadata, mParameters, fnGetEntity, bIgnoreETag, fnOnStrictHandlingFailed) {
var bAction = oOperationMetadata.$kind === "Action",
oCache,
vEntity = fnGetEntity,
oModel = this.oModel,
sMetaPath = _Helper.getMetaPath(sPath),
sOriginalResourcePath = sPath.slice(1),
oRequestor = oModel.oRequestor,
that = this;
/*
* Returns the original resource path to be used for bound messages.
*
* @param {object} The response entity
* @returns {string} The original resource path
*/
function getOriginalResourcePath(oResponseEntity) {
if (that.isReturnValueLikeBindingParameter(oOperationMetadata)) {
const sRVCPath = that.getReturnValueContextPath(oResponseEntity);
if (sRVCPath) {
return sRVCPath;
}
if (that.oOperation.bAdditionalQueryOptionsForRVC === false
&& _Helper.getPrivateAnnotation(vEntity, "predicate")
=== _Helper.getPrivateAnnotation(oResponseEntity, "predicate")) {
// return value is *same* as binding parameter: attach messages to the latter
return sOriginalResourcePath.slice(0, sOriginalResourcePath.lastIndexOf("/"));
}
}
return sOriginalResourcePath;
}
/*
* Calls back into the application with the messages whether to repeat the action.
* @param {Error} oError The error from the failed request
* @returns {Promise<boolean>} A promise resolving with a boolean
* @throws {Error} If <code>fnOnStrictHandlingFailed</code> does not return a promise
*/
function onStrictHandling(oError) {
var oResult;
_Helper.adjustTargetsInError(oError, oOperationMetadata,
that.oParameterContext.getPath(),
that.bRelative ? that.oContext.getPath() : undefined);
oError.error.$ignoreTopLevel = true;
oResult = fnOnStrictHandlingFailed(
_Helper.extractMessages(oError).map(function (oRawMessage) {
return oModel.createUI5Message(oRawMessage);
})
);
if (!(oResult instanceof Promise)) {
throw new Error("Not a promise: " + oResult);
}
return oResult;
}
if (fnOnStrictHandlingFailed && oOperationMetadata.$kind !== "Action") {
throw new Error("Not an action: " + sPath);
}
if (!bAction && oOperationMetadata.$kind !== "Function"
&& oOperationMetadata.$kind !== "NavigationProperty") {
throw new Error("Not an operation: " + sPath);
}
if (bAction && fnGetEntity) {
vEntity = fnGetEntity();
}
if (bIgnoreETag && !(bAction && oOperationMetadata.$IsBound && vEntity !== null)) {
throw new Error("Not a bound action: " + sPath);
}
if (this.bInheritExpandSelect
&& !this.isReturnValueLikeBindingParameter(oOperationMetadata)) {
throw new Error("Must not set parameter $$inheritExpandSelect on this binding");
}
if (oOperationMetadata.$kind !== "NavigationProperty") {
sMetaPath += "/@$ui5.overload/0/$ReturnType";
if (oOperationMetadata.$ReturnType
&& !oOperationMetadata.$ReturnType.$Type.startsWith("Edm.")) {
sMetaPath += "/$Type";
}
} else if (!_Helper.isEmptyObject(mParameters)) {
throw new Error("Unsupported parameters for navigation property");
}
if (that.oReturnValueContext) {
that.oReturnValueContext.destroy();
that.oReturnValueContext = null;
}
this.oOperation.bAction = bAction;
this.oOperation.mRefreshParameters = mParameters;
mParameters = Object.assign({}, mParameters);
this.mCacheQueryOptions = this.addQueryOptionsForReturnValueContext(oOperationMetadata,
this.computeOperationQueryOptions());
// Note: in case of NavigationProperty, this just removes "(...)"
sPath = oRequestor.getPathAndAddQueryOptions(sPath, oOperationMetadata, mParameters,
this.mCacheQueryOptions, vEntity);
oCache = _Cache.createSingle(oRequestor, sPath, this.mCacheQueryOptions,
oModel.bAutoExpandSelect, oModel.bSharedRequests, undefined, bAction,
sMetaPath);
this.oCache = oCache;
this.oCachePromise = SyncPromise.resolve(oCache);
return bAction
? oCache.post(oGroupLock, mParameters, vEntity, bIgnoreETag,
fnOnStrictHandlingFailed && onStrictHandling, getOriginalResourcePath)
: oCache.fetchValue(oGroupLock, "", undefined, undefined, false,
getOriginalResourcePath);
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataParentBinding#delete
*/
ODataContextBinding.prototype.delete = function (oGroupLock, sEditUrl, oContext, _oETagEntity,
bDoNotRequestCount, fnUndelete) {
// In case the context binding has an empty path, the respective context in the parent
// needs to be removed as well. As there could be more levels of bindings pointing to the
// same entity, first go up the binding hierarchy and find the context pointing to the same
// entity in the highest level binding.
// In case that top binding is a list binding, perform the deletion from there but use the
// ETag of this binding.
// In case the top binding is a context binding, perform the deletion from here but destroy
// the context(s) in that uppermost binding. Note that no data may be available in the
// uppermost context binding and hence the deletion would not work there, BCP 1980308439.
var oEmptyPathParentContext = this._findEmptyPathParentContext(this.oElementContext),
oEmptyPathParentBinding = oEmptyPathParentContext.getBinding(),
oDeleteParentContext = oEmptyPathParentBinding.getContext(),
oReturnValueContext = oEmptyPathParentBinding.oReturnValueContext,
that = this;
function undelete() {
fnUndelete();
oEmptyPathParentContext.oDeletePromise = null;
}
// In case the uppermost parent reached with empty paths is a list binding, delete there.
if (!oEmptyPathParentBinding.invoke) {
// In the Cache, the request is generated with a reference to the entity data
// first. So, hand over the complete entity to have the ETag of the correct binding
// in the request.
// oEmptyPathParentContext is marked as deleted in delete(), mark oContext too
oContext.oDeletePromise = oEmptyPathParentBinding.delete(oGroupLock, sEditUrl,
oEmptyPathParentContext, oContext.getValue(), bDoNotRequestCount, undelete
);
return oContext.oDeletePromise;
}
oEmptyPathParentBinding.oElementContext = null;
if (oReturnValueContext) {
oEmptyPathParentBinding.oReturnValueContext = null;
}
this._fireChange({reason : ChangeReason.Remove});
// oEmptyPathParentContext is marked as deleted in doDelete(), mark oContext too
oContext.oDeletePromise = oEmptyPathParentContext.doDelete(oGroupLock, sEditUrl, "", null,
this, function (_iIndex, iOffset) {
if (iOffset > 0) {
undelete();
}
}
).then(function () {
oEmptyPathParentContext.destroy();
if (oReturnValueContext) {
oReturnValueContext.destroy();
}
}, function (oError) {
// if the cache has become inactive, the callback is not called -> undelete here
undelete();
if (!oEmptyPathParentBinding.isRelative()
|| oDeleteParentContext === oEmptyPathParentBinding.getContext()) {
oEmptyPathParentBinding.oElementContext = oEmptyPathParentContext;
if (oReturnValueContext) {
oEmptyPathParentBinding.oReturnValueContext = oReturnValueContext;
}
that._fireChange({reason : ChangeReason.Add});
}
throw oError;
});
return oContext.oDeletePromise;
};
/**
* Destroys the object. The object must not be used anymore after this function was called.
*
* @public
* @see sap.ui.model.Binding#destroy
* @since 1.40.1
*/
// @override sap.ui.model.Binding#destroy
ODataContextBinding.prototype.destroy = function () {
if (this.oElementContext) {
this.oElementContext.destroy();
this.oElementContext = undefined;
}
if (this.oParameterContext) {
this.oParameterContext.destroy();
this.oParameterContext = undefined;
}
if (this.oReturnValueContext) {
this.oReturnValueContext.destroy();
this.oReturnValueContext = undefined;
}
this.oModel.bindingDestroyed(this);
this.oOperation = undefined;
this.mParameters = undefined;
this.mQueryOptions = undefined;
asODataParentBinding.prototype.destroy.call(this);
ContextBinding.prototype.destroy.call(this);
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#doCreateCache
*/
ODataContextBinding.prototype.doCreateCache = function (sResourcePath, mQueryOptions, _oContext,
sDeepResourcePath) {
return _Cache.createSingle(this.oModel.oRequestor, sResourcePath, mQueryOptions,
this.oModel.bAutoExpandSelect, this.oModel.bSharedRequests, sDeepResourcePath);
};
/**
* Fetches all properties described in $expand and $select of the binding parameters, unless
* the binding already has fetched it. This is only done if the model uses autoExpandSelect. The
* goal is that these properties are also requested as late properties.
*
* Expects that the binding is resolved and has no own cache (and thus a parent context). This
* together with autoExpandSelect also implies that $expand contains no collection-valued
* navigation properties.
*
* @private
*/
ODataContextBinding.prototype.doFetchExpandSelectProperties = function () {
var sResolvedPath,
that = this;
if (this.bHasFetchedExpandSelectProperties || !this.oModel.bAutoExpandSelect
|| !this.mParameters.$expand && !this.mParameters.$select) {
return;
}
sResolvedPath = this.getResolvedPath();
_Helper.convertExpandSelectToPaths(this.oModel.buildQueryOptions(this.mParameters, true))
.forEach(function (sPath) {
that.oContext.fetchValue(_Helper.buildPath(sResolvedPath, sPath))
.catch(that.oModel.getReporter());
});
this.bHasFetchedExpandSelectProperties = true;
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#doFetchOrGetQueryOptions
*/
ODataContextBinding.prototype.doFetchOrGetQueryOptions = function (oContext) {
return this.fetchResolvedQueryOptions(oContext);
};
/**
* Handles setting a parameter property in case of a deferred operation binding, otherwise it
* returns <code>undefined</code>.
*
* @private
*/
// @override sap.ui.model.odata.v4.ODataParentBinding#doSetProperty
ODataContextBinding.prototype.doSetProperty = function (sPath, vValue, oGroupLock) {
if (this.oOperation && (sPath === "$Parameter" || sPath.startsWith("$Parameter/"))) {
_Helper.updateAll(this.oOperation.mChangeListeners, "", this.oOperation.mParameters,
_Helper.makeUpdateData(sPath.split("/").slice(1), vValue));
this.oOperation.bAction = undefined; // "not yet invoked"
if (oGroupLock) {
oGroupLock.unlock();
}
return SyncPromise.resolve();
}
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataParentBinding#doSuspend
*/
ODataContextBinding.prototype.doSuspend = function () {
if (this.bInitial && !this.oOperation) {
// if the binding is still initial, it must fire an event in resume
this.sResumeChangeReason = ChangeReason.Change;
}
};
/**
* Requests the value for the given path; the value is requested from this binding's
* cache or from its context in case it has no cache. For a suspended binding, requesting the
* value is canceled by throwing a "canceled" error.
*
* @param {string} sPath
* Some absolute path
* @param {sap.ui.model.odata.v4.ODataPropertyBinding} [oListener]
* A property binding which registers itself as listener at the cache
* @param {boolean} [bCached]
* Whether to return cached values only and not initiate a request
* @returns {sap.ui.base.SyncPromise<any>}
* A promise on the outcome of the cache's <code>fetchValue</code> call; it is rejected in
* case cached values are asked for, but not found, or if the cache is no longer the active
* cache when the response arrives
* @throws {Error} If the binding's root binding is suspended, a "canceled" error is thrown
*
* @private
*/
ODataContextBinding.prototype.fetchValue = function (sPath, oListener, bCached) {
var oCachePromise = bCached && this.oCache !== undefined
? SyncPromise.resolve(this.oCache)
: this.oCachePromise,
that = this;
// dependent binding will update its value when the suspended binding is resumed
if (this.isRootBindingSuspended()) {
const oError = new Error("Suspended binding provides no value");
oError.canceled = "noDebugLog";
throw oError;
}
return oCachePromise.then(function (oCache) {
var bPreventBubbling,
bDataRequested = false,
oGroupLock,
sResolvedPath = that.getResolvedPath(),
sRelativePath = oCache || that.oOperation
? that.getRelativePath(sPath)
: undefined,
aSegments;
if (that.oOperation) {
if (sRelativePath === undefined) {
// a reduced path to a property of the binding parameter
return that.oContext.fetchValue(sPath, oListener, bCached);
}
aSegments = sRelativePath.split("/");
if (aSegments[0] === "$Parameter") {
if (aSegments.length === 1) {
return undefined;
}
_Helper.registerChangeListener(that.oOperation,
sRelativePath.slice(/*"$Parameter/".length*/11), oListener);
const vValue = _Helper.drillDown(that.oOperation.mParameters,
aSegments.slice(1));
return vValue === undefined ? null : vValue;
}
}
if (oCache && sRelativePath !== undefined) {
if (bCached) {
oGroupLock = _GroupLock.$cached;
} else {
oGroupLock = that.oReadGroupLock || that.lockGroup();
that.oReadGroupLock = undefined;
}
bPreventBubbling = that.isRefreshWithoutBubbling();
return that.resolveRefreshPromise(
oCache.fetchValue(oGroupLock, sRelativePath, function () {
bDataRequested = true;
that.fireDataRequested(bPreventBubbling);
}, oListener)
).then(function (vValue) {
that.checkSameCache(oCache);
return vValue;
}).then(function (vValue) {
if (bDataRequested) {
that.fireDataReceived({data : {}}, bPreventBubbling);
}
return vValue;
}, function (oError) {
oGroupLock.unlock(true);
if (bDataRequested) {
that.oModel.reportError("Failed to read path " + sResolvedPath, sClassName,
oError);
that.fireDataReceived(oError.canceled ? {data : {}} : {error : oError},
bPreventBubbling);
}
throw oError;
});
}
if (!that.oOperation && that.oContext) {
if (!bCached) {
that.doFetchExpandSelectProperties();
}
return that.oContext.fetchValue(sPath, oListener, bCached);
}
});
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataParentBinding#findContextForCanonicalPath
*/
ODataContextBinding.prototype.findContextForCanonicalPath = function (sCanonicalPath) {
var oContext = this.oOperation ? this.oReturnValueContext : this.oElementContext,
oEntity,
oPromise;
if (oContext) {
oEntity = oContext.getValue();
// avoid problems in fetchCanonicalPath (leading to an ODM#reportError)
if (oEntity && _Helper.hasPrivateAnnotation(oEntity, "predicate")) {
oPromise = oContext.fetchCanonicalPath();
oPromise.caught();
if (oPromise.getResult() === sCanonicalPath) {
return oContext;
}
}
}
};
/**
* Returns the bound context.
*
* @returns {sap.ui.model.odata.v4.Context}
* The bound context
*
* @function
* @name sap.ui.model.odata.v4.ODataContextBinding#getBoundContext
* @public
* @since 1.39.0
*/
/**
* @override
* @see sap.ui.model.odata.v4.ODataBinding#getDependentBindings
*/
ODataContextBinding.prototype.getDependentBindings = function () {
return this.oModel.getDependentBindings(this);
};
/**
* Returns the context pointing to the parameters of a deferred operation binding.
*
* @returns {sap.ui.model.odata.v4.Context}
* The parameter context
* @throws {Error}
* If the binding is not a deferred operation binding (see
* {@link sap.ui.model.odata.v4.ODataContextBinding})
*
* @public
* @since 1.73.0
*/
ODataContextBinding.prototype.getParameterContext = function () {
if (!this.oOperation) {
throw new Error("Not a deferred operation binding: " + this);
}
return this.oParameterContext;
};
/**
* @override
* @see sap.ui.model.odata.v4.ODataParentBinding#getQueryOptionsFromParameters
*/
ODataContextBinding.prototype.getQueryOptionsFromParameters = function () {
var mInheritableQueryOptions,
mQueryOptions = this.mQueryOptions;
if (this.bInheritExpandSelect) {
mInheritableQueryOptions = this.oContext.getBinding().getInheritableQueryOptions();
mQueryOptions = Object.assign({}, mQueryOptions);
// keep $select before $expand
if ("$select" in mInheritableQueryOptions) {
// avoid that this.mQueryOptions.$select is modified
mQueryOptions.$select &&= mQueryOptions.$select.slice();
_Helper.addToSelect(mQueryOptions, mInheritableQueryOptions.$select);
}
if ("$expand" in mInheritableQueryOptions) {
mQueryOptions.$expand = mInheritableQueryOptions.$expand;
}
}
return mQueryOptions;
};
/**
* Returns the resolved path, replacing all occurrences of transient predicates with the
* corresponding key predicates.
*
* @returns {string}
* The resolved path with replaced transient predicates
* @throws {Error}
* If an entity related to a segment with a transient predicate does not have a key predicate
*
* @private
*/
ODataContextBinding.prototype.getResolvedPathWithReplacedTransientPredicates = function () {
var sPath = "",
sResolvedPath = this.getResolvedPath(),
aSegments,
that = this;
if (sResolvedPath && sResolvedPath.includes("($uid=")) {
aSegments = sResolvedPath.slice(1).split("/");
sResolvedPath = "";
aSegments.forEach(function (sSegment) {
var oEntity, sPredicate, iTransientPredicate;
sPath += "/" + sSegment;
iTransientPredicate = sSegment.indexOf("($uid=");
if (iTransientPredicate >= 0) {
oEntity = that.oContext.getValue(sPath);
sPredicate = oEntity && _Helper.getPrivateAnnotation(oEntity, "predicate");
if (!sPredicate) {
throw new Error("No key predicate known at " + sPath);
}
sResolvedPath += "/" + sSegment.slice(0, iTransientPredicate) + sPredicate;
} else {
sResolvedPath += "/" + sSegment;
}
});
}
return sResolvedPath;
};
/**
* Returns the path for the return value context. Supports bound operations on an entity or
* a collection.
*
* @param {object} oResponseEntity
* The result of the invoked operation
* @returns {string|undefined}
* The path for the return value context, but w/o(!) the initial slash, or
* <code>undefined</code> if it is not possible to create one
*
* @private
*/
ODataContextBinding.prototype.getReturnValueContextPath = function (oResponseEntity) {
if (!this.hasReturnValueContext()) {
return undefined;
}
const sBindingParameterPath = this.oContext.getPath().slice(1);
const sPredicate = _Helper.getPrivateAnnotation(oResponseEntity, "predicate");
if (this.oOperation.bAdditionalQueryOptionsForRVC === false) {
const i = sBindingParameterPath.indexOf("(");
return (i < 0 ? sBindingParameterPath : sBindingParameterPath.slice(0, i)) + sPredicate;
}
const aMetaPathSegments = _Helper.getMetaPath(sBindingParameterPath).split("/");
const sPartner = this.oModel.getMetaModel()
.getObject("/" + aMetaPathSegments[0] + "/" + aMetaPathSegments[1] + "/$Partner");
const oPartner = oResponseEntity[sPartner];
const sPartnerPredicate
= oPartner && this.oModel.getKeyPredicate("/" + aMetaPathSegments[0], oPartner);
if (!(sPartnerPredicate && sPredicate)) {
return undefined;
}
return sBindingParameterPath.split("/").map((sSegment, i) => {
return sSegment.slice(0, sSegment.indexOf("("))
+ (i ? sPredicate : sPartnerPredicate);
}).join("/");
};
/**
* Handles the result of an invoked operation and creates a return value context if possible.
*
* @param {object} oOperationMetadata
* The operation's metadata
* @param {object} oResponseEntity
* The result of the invoked operation
* @param {boolean} [bReplaceWithRVC]
* Whether this operation binding's parent context, which must belong to a list binding, is
* replaced with the operation's return value context and that new list context is returned
* instead.
* @returns {sap.ui.model.odata.v4.Context}
* The return value context or <code>undefined</code> if it is not possible to create one
* @throws {Error}
* If <code>bReplaceWithRVC</code> is given, but no return value context can be created
*
* @private
*/
ODataContextBinding.prototype.handleOperationResult = function (oOperationMetadata,
oResponseEntity, bReplaceWithRVC) {
var sContextPredicate, oOldValue, sResponsePredicate, sNewPath, oResult;
if (this.isReturnValueLikeBindingParameter(oOperationMetadata)) {
oOldValue = this.oContext.getValue();
// Note: sContextPredicate missing e.g. when collection-bound
sContextPredicate = oOldValue && _Helper.getPrivateAnnotation(oOldValue, "predicate");
sResponsePredicate = _Helper.getPrivateAnnotation(oResponseEntity, "predicate");
if (sResponsePredicate) {
if (sContextPredicate === sResponsePredicate) {
// this is sync, because the entity to be patched is available in
// the context (we already read its predicate)
this.oContext.patch(oResponseEntity);
}
sNewPath = this.getReturnValueContextPath(oResponseEntity); // w/o initial "/"!
if (sNewPath) {
if (bReplaceWithRVC) {
// replace is only possible if the path does not contain any navigation
// property or the key predicate of the first segment has not changed!
if (this.oOperation.bAdditionalQueryOptionsForRVC
&& this.oContext.getPath().split("/")[1]
!== sNewPath.split("/")[0]) {
throw new Error("Cannot replace due to changed key predicate"
+ " for navigation property in path");
}
this.oCache = null;
this.oCachePromise = SyncPromise.resolve(null);
oResult = this.oContext.getPath().indexOf(sNewPath) === 1
? this.oContext
: this.oContext.getBinding()
.doReplaceWith(this.oContext, oResponseEntity, sResponsePredicate);
oResult.setNewGeneration();
return oResult;
}
this.oReturnValueContext = Context.createNewContext(this.oModel,
this, "/" + sNewPath);
// set the resource path for late property requests
this.oCache.setResourcePath(sNewPath);
return this.oReturnValueContext;
}
}
}
if (bReplaceWithRVC) {
throw new Error("Cannot replace w/o return value context");
}
};
/**
* Determines whether an operation binding creates a return value context on {@link #invoke}.
* The following conditions must hold for a return value context to be created:
* 1. Operation is bound.
* 2. Operation has single entity return value. Note: existence of EntitySetPath
* implies the return value is an entity or a collection thereof;
* see [OData-CSDL-XML-v4.01], 12.6. It thus ensures the "entity" in this condition.
* 3. EntitySetPath of operation is the binding parameter.
* 4. Operation binding has
* (a) a V4 parent context which
* (b) points to an entity from an entity set or the entity set itself w/ a maximum of one
* navigation property.
*
* BEWARE: It is the caller's duty to check 1. through 4.(a) via
* {@link #isReturnValueLikeBindingParameter}!
*
* BEWARE: In {@link #addQueryOptionsForReturnValueContext} the flag
* <code>this.oOperation.bAdditionalQueryOptionsForRVC</code> ist set. Until this is done this
* function will also return true, because it seems possible to create a return value context.
* If it was possbile to determine the additional needed query options in
* {@link #addQueryOptionsForReturnValueContext}, we can be sure that it is possible to create a
* return value context.
*
* @returns {boolean} Whether it seems possible to create a return value context
*
* @private
*/
ODataContextBinding.prototype.hasReturnValueContext = function () {
var aMetaSegments = _Helper.getMetaPath(this.getResolvedPath()).split("/");
if (aMetaSegments.length === 4) {
return this.oOperation.bAdditionalQueryOptionsForRVC !== false;
}
return aMetaSegments.length === 3
&& this.oModel.getMetaModel().getObject("/" + aMetaSegments[1]).$kind === "EntitySet";
};
/**
* Initializes the OData context binding: Fires a 'change' event in case the binding has a
* resolved path and its root binding is not suspended.
*
* @protected
* @see #getRootBinding
* @since 1.37.0
*/
// @override sap.ui.model.Binding#initialize
ODataContextBinding.