UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,197 lines (1,117 loc) 67.3 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 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 execution 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 #execute}. {@link #refresh} is always * ignored for actions and action imports. For bound functions and function imports, it is * ignored if {@link #execute} has not yet been called. Afterwards it results in another * call of the function with the parameter values of the last execute. * * 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.111.5 * * @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 }); /** * Returns the path for the return value context. Supports bound operations on an entity or a * collection. * * @param {string} sPath * The bindings's path; either a resolved model path or a resource path; for example: * "Artists(ArtistID='42',IsActiveEntity=true)/special.cases.EditAction(...)" or * "/Artists(ArtistID='42',IsActiveEntity=true)/special.cases.EditAction(...)" or * "Artists/special.cases.Create(...)" or "/Artists/special.cases.Create(...)" * @param {string} sResponsePredicate The key predicate of the response entity * @returns {string} The path for the return value context. */ function getReturnValueContextPath(sPath, sResponsePredicate) { var sBoundParameterPath = sPath.slice(0, sPath.lastIndexOf("/")), i = sBoundParameterPath.indexOf("("); return (i < 0 ? sBoundParameterPath : sPath.slice(0, i)) + sResponsePredicate; } //********************************************************************************************* // 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, mChangeListeners : {}, // map from path to an array of change listeners mParameters : {}, sResourcePath : undefined }; 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 execute * @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} * 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 #execute for details */ ODataContextBinding.prototype._execute = 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} A promise resolving when the refresh is finished */ function fireChangeAndRefreshDependentBindings() { that._fireChange({reason : ChangeReason.Change}); return that.refreshDependentBindings("", oGroupLock.getGroupId(), true); } oPromise = oMetaModel.fetchObject(sResolvedMetaPath + "/@$ui5.overload") .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 () { var sContextPredicate, oOldValue, sResponsePredicate, oResult; if (that.isReturnValueLikeBindingParameter(oOperationMetadata)) { oOldValue = that.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) that.oContext.patch(oResponseEntity); } if (that.hasReturnValueContext()) { if (bReplaceWithRVC) { that.oCache = null; that.oCachePromise = SyncPromise.resolve(null); oResult = that.oContext.getBinding() .doReplaceWith(that.oContext, oResponseEntity, sResponsePredicate); oResult.setNewGeneration(); return oResult; } that.oReturnValueContext = Context.createNewContext(that.oModel, that, getReturnValueContextPath(sResolvedPath, sResponsePredicate)); // set the resource path for late property requests that.oCache.setResourcePath( that.oReturnValueContext.getPath().slice(1)); return that.oReturnValueContext; } } } if (bReplaceWithRVC) { throw new Error("Cannot replace w/o return value context"); } }); }, 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 execute " + sResolvedPath, sClassName, oError); throw oError; }); return Promise.resolve(oPromise); }; /** * @override * @see sap.ui.model.odata.v4.ODataBinding#adjustPredicate */ ODataContextBinding.prototype.adjustPredicate = function (sTransientPredicate, sPredicate) { asODataParentBinding.prototype.adjustPredicate.apply(this, arguments); if (this.mCacheQueryOptions) { // Note: this.oCache === null because of special case in #createAndSetCache this.fetchCache(this.oContext, true); } if (this.oElementContext) { this.oElementContext.adjustPredicate(sTransientPredicate, sPredicate); } // this.oReturnValueContext cannot have the transient predicate; it results from #execute // 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.execute().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 * @param {object} oEvent.getParameters() * @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 #execute}), 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 #execute} 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 * @param {function} oEvent.cancelBubble * A callback function to prevent that the event is bubbled up to the model * @param {object} oEvent.getParameters() * @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 #execute}. * * 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 {object} oEvent.getParameters() 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); }; /** * Returns this operation binding's cache query options. * * @returns {object} The query options * * @private */ ODataContextBinding.prototype.computeOperationQueryOptions = function () { return Object.assign({}, this.oModel.mUriParameters, this.getQueryOptionsFromParameters()); }; /** * @override * @see sap.ui.model.odata.v4.ODataParentBinding#checkKeepAlive */ ODataContextBinding.prototype.checkKeepAlive = function () { throw new Error("Unsupported " + this); }; /** * 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 execute * @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} * 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)) { if (that.hasReturnValueContext()) { return getReturnValueContextPath(sOriginalResourcePath, _Helper.getPrivateAnnotation(oResponseEntity, "predicate")); } if (_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} 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 that.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)) { 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 (Object.keys(mParameters).length) { 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.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.execute) { // 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); }; /** * @override * @see sap.ui.model.odata.v4.ODataBinding#doDeregisterChangeListener */ ODataContextBinding.prototype.doDeregisterChangeListener = function (sPath, oListener) { if (this.oOperation && (sPath === "$Parameter" || sPath.startsWith("$Parameter/"))) { _Helper.removeByPath(this.oOperation.mChangeListeners, sPath.slice(/*"$Parameter/".length*/11), oListener); return; } asODataParentBinding.prototype.doDeregisterChangeListener.apply(this, arguments); }; /** * @override * @see sap.ui.model.odata.v4.ODataBinding#doFetchQueryOptions */ ODataContextBinding.prototype.doFetchQueryOptions = function (oContext) { return this.fetchResolvedQueryOptions(oContext); }; /** * Handles setting a parameter property in case of a deferred operation binding, otherwise it * returns <code>undefined</code>. */ // @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, _Cache.makeUpdateData(sPath.split("/").slice(1), vValue)); this.oOperation.bAction = undefined; // "not yet executed" 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; } }; /** * Calls the OData operation that corresponds to this operation binding. * * Parameters for the operation must be set via {@link #setParameter} beforehand. * * The value of this binding is the result of the operation. To access a result of primitive * type, bind a control to the path "value", for example * <code>&lt;Text text="{value}"/></code>. If the result has a complex or entity type, you * can bind properties as usual, for example <code>&lt;Text text="{street}"/></code>. * * Since 1.98.0, a single-valued navigation property can be treated like a function if * <ul> * <li> it has the same type as the operation binding's parent context, * <li> that parent context is in a list binding for a top-level entity set, * <li> there is a navigation property binding which points to that same entity set, * <li> no operation parameters have been set, * <li> the <code>bReplaceWithRVC</code> parameter is used. * </ul> * * @param {string} [sGroupId] * The group ID to be used for the request; if not specified, the group ID for this binding is * used, see {@link sap.ui.model.odata.v4.ODataContextBinding#constructor} and * {@link #getGroupId}. To use the update group ID, see {@link #getUpdateGroupId}, it needs to * be specified explicitly. * Valid values are <code>undefined</code>, '$auto', '$auto.*', '$direct' or application group * IDs as specified in {@link sap.ui.model.odata.v4.ODataModel}. * @param {boolean} [bIgnoreETag] * Whether the entity's ETag should be actively ignored (If-Match:*); supported for bound * actions only, since 1.90.0. Ignored if there is no ETag (since 1.93.0). * @param {function} [fnOnStrictHandlingFailed] * If this callback is given for an action, the preference "handling=strict" is applied. If * the service responds with the HTTP status code 412 and a * "Preference-applied: handling=strict" header, the details from the OData error response are * extracted and passed to the callback as an array of {@link sap.ui.core.message.Message} * items. The callback has to return a <code>Promise</code> resolving with a * <code>boolean</code> value in order to indicate whether the bound action should either be * repeated <b>without</b> applying the preference or rejected with an <code>Error</code> * instance <code>oError</code> where <code>oError.canceled === true</code>. * Since 1.92.0. * @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 list context is * returned instead. That list context may be a newly created context or an existing context. * A newly created context has the same <code>keepAlive</code> attribute and * <code>fnOnBeforeDestroy</code> function as the parent context, see * {@link sap.ui.model.odata.v4.Context#setKeepAlive}; <code>fnOnBeforeDestroy</code> will be * called with the new context instance as the only argument in this case. An existing context * does not change its <code>keepAlive</code> attribute. In any case, the resulting context * takes the place (index, position) of the parent context (see * {@link sap.ui.model.odata.v4.Context#getIndex}), which need not be in the collection * currently if it is {@link sap.ui.model.odata.v4.Context#isKeepAlive kept alive}. If the * parent context has requested messages when it was kept alive, they will be inherited if the * $$inheritExpandSelect binding parameter is set to <code>true</code>. Since 1.97.0. * @returns {Promise} * 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, for instance if the operation metadata is not found, if overloading is not * supported, if a collection-valued function parameter is encountered, or if * <code>bIgnoreETag</code> is used for an operation other than a bound action. It is also * rejected if <code>fnOnStrictHandlingFailed</code> is supplied and * <ul> * <li> is used for an operation other than an action, * <li> another request that applies the preference "handling=strict" exists in a different * change set of the same $batch request, * <li> it does not return a <code>Promise</code>, * <li> returns a <code>Promise</code> that resolves with <code>false</code>. In this case * <code>oError.canceled === true</code>. * </ul> * It is also rejected if <code>bReplaceWithRVC</code> is supplied, and there is no return * value context at all or the existing context as described above is currently part of the * list's collection (that is, has an index). * <br> * A return value context is an {@link sap.ui.model.odata.v4.Context} which represents a bound * operation response. It is created only if the operation is bound and has a single entity * return value from the same entity set as the operation's binding parameter and has a * parent context which is an {@link sap.ui.model.odata.v4.Context} and points to an entity * from an entity set. It is destroyed the next time this operation binding is executed again! * <br> * If a return value context is created, it must be used instead of * <code>this.getBoundContext()</code>. All bound messages will be related to the return value * context only. Such a message can only be connected to a corresponding control if the * control's property bindings use the return value context as binding context. * @throws {Error} If * <ul> * <li> the binding's root binding is suspended, * <li> the given group ID is invalid, * <li> the binding is not a deferred operation binding (see * {@link sap.ui.model.odata.v4.ODataContextBinding}), * <li> the binding is unresolved (see * {@link sap.ui.model.Binding#isResolved}) * <li> the binding is relative to a transient context (see * {@link sap.ui.model.odata.v4.Context#isTransient}), * <li> deferred operation bindings are nested, * <li> the OData resource path for a deferred operation binding's context cannot be * determined, * <li> <code>bReplaceWithRVC</code> is given, but this operation binding is not relative to * a row context of a list binding which uses the <code>$$ownRequest</code> parameter (see * {@link sap.ui.model.odata.v4.ODataModel#bindList}) and no data aggregation (see * {@link sap.ui.model.odata.v4.ODataListBinding#setAggregation}). * * @public * @since 1.37.0 */ ODataContextBinding.prototype.execute = function (sGroupId, bIgnoreETag, fnOnStrictHandlingFailed, bReplaceWithRVC) { var sResolvedPath = this.getResolvedPath(); this.checkSuspended(); _Helper.checkGroupId(sGroupId); if (!this.oOperation) { throw new Error("The binding must be deferred: " + this.sPath); } if (this.bRelative) { if (!sResolvedPath) { throw new Error("Unresolved binding: " + this.sPath); } if (this.oContext.isTransient && this.oContext.isTransient()) { throw new Error("Execute for transient context not allowed: " + sResolvedPath); } if (this.oContext.getPath().includes("(...)")) { throw new Error("Nested deferred operation bindings not supported: " + sResolvedPath); } if (bReplaceWithRVC) { if (!this.oContext.getBinding) { throw new Error("Cannot replace this parent context: " + this.oContext); } // Note: parent context need not have a key predicate! this.oContext.getBinding().checkKeepAlive(this.oContext); } } else if (bReplaceWithRVC) { throw new Error("Cannot replace when operation is not relative"); } return this._execute(this.lockGroup(sGroupId, true), _Helper.publicClone(this.oOperation.mParameters, true), bIgnoreETag, fnOnStrictHandlingFailed, bReplaceWithRVC); }; /** * 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; }; /** * 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 trigger a request * @returns {sap.ui.base.SyncPromise} * 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, oError, oRootBinding = this.getRootBinding(), that = this; // dependent binding will update its value when the suspended binding is resumed if (oRootBinding && oRootBinding.isSuspended()) { 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, vValue; 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.addByPath(that.oOperation.mChangeListeners, sRelativePath.slice(/*"$Parameter/".length*/11), oListener); 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.assertSameCache(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) { // use absolute path to avoid unnecessary (but poss. failing) path reduction oEntity = oContext.getValue(oContext.getPath()); // 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