UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,417 lines (1,320 loc) 139 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.ODataMetaModel sap.ui.define([ "./AnnotationHelper", "./ValueListType", "./lib/_Helper", "sap/base/assert", "sap/base/Log", "sap/base/util/isEmptyObject", "sap/base/util/JSTokenizer", "sap/base/util/ObjectPath", "sap/ui/base/ManagedObject", "sap/ui/base/SyncPromise", "sap/ui/model/BindingMode", "sap/ui/model/ChangeReason", "sap/ui/model/ClientListBinding", "sap/ui/model/Context", "sap/ui/model/ContextBinding", "sap/ui/model/MetaModel", "sap/ui/model/PropertyBinding", "sap/ui/model/odata/OperationMode", // load all modules for predefined OData types upfront so that ODataPropertyBinding#checkUpdate // does not lead to a new task just because the module of the auto-detected type is not loaded "sap/ui/model/odata/type/Boolean", "sap/ui/model/odata/type/Byte", "sap/ui/model/odata/type/Date", "sap/ui/model/odata/type/DateTimeOffset", "sap/ui/model/odata/type/Decimal", "sap/ui/model/odata/type/Double", "sap/ui/model/odata/type/Guid", "sap/ui/model/odata/type/Int16", "sap/ui/model/odata/type/Int32", "sap/ui/model/odata/type/Int64", "sap/ui/model/odata/type/Raw", "sap/ui/model/odata/type/SByte", "sap/ui/model/odata/type/Single", "sap/ui/model/odata/type/Stream", "sap/ui/model/odata/type/String", "sap/ui/model/odata/type/TimeOfDay", "sap/ui/thirdparty/URI" ], function (AnnotationHelper, ValueListType, _Helper, assert, Log, isEmptyObject, JSTokenizer, ObjectPath, ManagedObject, SyncPromise, BindingMode, ChangeReason, ClientListBinding, BaseContext, ContextBinding, MetaModel, PropertyBinding, OperationMode, Boolean, Byte, EdmDate, DateTimeOffset, Decimal, Double, Guid, Int16, Int32, Int64, Raw, SByte, Single, Stream, String, TimeOfDay, URI) { "use strict"; /*eslint max-nested-callbacks: 0 */ var Any = ManagedObject.extend("sap.ui.model.odata.v4._any", { metadata : { properties : { any : "any" } } }), oCountType, mCodeListUrl2Promise = new Map(), DEBUG = Log.Level.DEBUG, rLeftBraces = /\$\(/g, rNumber = /^-?\d+$/, sODataMetaModel = "sap.ui.model.odata.v4.ODataMetaModel", rPredicate = /\(.*\)$/, oRawType = new Raw(), rRightBraces = /\$\)/g, mSharedModelByUrl = new Map(), mSupportedEvents = { messageChange : true }, mUi5TypeForEdmType = { "Edm.Boolean" : {Type : Boolean}, "Edm.Byte" : {Type : Byte}, "Edm.Date" : {Type : EdmDate}, "Edm.DateTimeOffset" : { constraints : { $Precision : "precision" }, Type : DateTimeOffset }, "Edm.Decimal" : { constraints : { "@Org.OData.Validation.V1.Minimum/$Decimal" : "minimum", "@Org.OData.Validation.V1.Minimum@Org.OData.Validation.V1.Exclusive" : "minimumExclusive", "@Org.OData.Validation.V1.Maximum/$Decimal" : "maximum", "@Org.OData.Validation.V1.Maximum@Org.OData.Validation.V1.Exclusive" : "maximumExclusive", $Precision : "precision", $Scale : "scale" }, Type : Decimal }, "Edm.Double" : {Type : Double}, "Edm.Guid" : {Type : Guid}, "Edm.Int16" : {Type : Int16}, "Edm.Int32" : {Type : Int32}, "Edm.Int64" : {Type : Int64}, "Edm.SByte" : {Type : SByte}, "Edm.Single" : {Type : Single}, "Edm.Stream" : {Type : Stream}, "Edm.String" : { constraints : { "@com.sap.vocabularies.Common.v1.IsDigitSequence" : "isDigitSequence", $MaxLength : "maxLength" }, Type : String }, "Edm.TimeOfDay" : { constraints : { $Precision : "precision" }, Type : TimeOfDay } }, UNBOUND = {}, sValueList = "@com.sap.vocabularies.Common.v1.ValueList", sValueListMapping = "@com.sap.vocabularies.Common.v1.ValueListMapping", sValueListReferences = "@com.sap.vocabularies.Common.v1.ValueListReferences", sValueListRelevantQualifiers = "@com.sap.vocabularies.Common.v1.ValueListRelevantQualifiers", sValueListWithFixedValues = "@com.sap.vocabularies.Common.v1.ValueListWithFixedValues", WARNING = Log.Level.WARNING, ODataMetaContextBinding, /** * Do <strong>NOT</strong> call this private constructor, but rather use * {@link sap.ui.model.odata.v4.ODataModel#getMetaModel} instead. * * @param {object} oRequestor * The metadata requestor * @param {string} sUrl * The URL to the $metadata document of the service * @param {string|string[]} [vAnnotationUri] * The URL (or an array of URLs) from which the annotation metadata are loaded * Supported since 1.41.0 * @param {sap.ui.model.odata.v4.ODataModel} oModel * The model this meta model is related to * @param {boolean} [bSupportReferences=true] * Whether <code>&lt;edmx:Reference></code> and <code>&lt;edmx:Include></code> directives * are supported in order to load schemas on demand from other $metadata documents and * include them into the current service ("cross-service references"). * * @alias sap.ui.model.odata.v4.ODataMetaModel * @author SAP SE * @class Implementation of an OData metadata model which offers access to OData V4 * metadata. The meta model does not support any public events; attaching an event handler * leads to an error. * * This model is read-only. * * This model is not prepared to be inherited from. * * @extends sap.ui.model.MetaModel * @hideconstructor * @public * @since 1.37.0 * @version 1.111.5 */ ODataMetaModel = MetaModel.extend("sap.ui.model.odata.v4.ODataMetaModel", { constructor : constructor }), ODataMetaListBinding, ODataMetaPropertyBinding; /** * Adds the given reference URI to the map of reference URIs for schemas. * * @param {sap.ui.model.odata.v4.ODataMetaModel} oMetaModel * The OData metadata model * @param {string} sSchema * A namespace of a schema, for example "foo.bar." * @param {string} sReferenceUri * A URI to the metadata document for the given schema * @param {string} [sDocumentUri] * The URI to the metadata document containing the given reference to the given schema * @throws {Error} * If the schema has already been loaded from a different URI */ function addUrlForSchema(oMetaModel, sSchema, sReferenceUri, sDocumentUri) { var sUrl0, mUrls = oMetaModel.mSchema2MetadataUrl[sSchema]; if (!mUrls) { mUrls = oMetaModel.mSchema2MetadataUrl[sSchema] = {}; mUrls[sReferenceUri] = false; } else if (!(sReferenceUri in mUrls)) { sUrl0 = Object.keys(mUrls)[0]; if (mUrls[sUrl0]) { // document already processed, no different URLs allowed reportAndThrowError(oMetaModel, "A schema cannot span more than one document: " + sSchema + " - expected reference URI " + sUrl0 + " but instead saw " + sReferenceUri, sDocumentUri); } mUrls[sReferenceUri] = false; } } /** * Returns the schema with the given namespace, or a promise which is resolved as soon as the * schema has been included, or <code>undefined</code> in case the schema is neither present nor * referenced. * * @param {sap.ui.model.odata.v4.ODataMetaModel} oMetaModel * The OData metadata model * @param {object} mScope * The $metadata "JSON" of the root service * @param {string} sSchema * A namespace, for example "foo.bar.", of a schema. * @param {function} fnLog * The log function * @returns {object|sap.ui.base.SyncPromise|undefined} * The schema, or a promise which is resolved without details or rejected with an error, or * <code>undefined</code>. * @throws {Error} * If the schema has already been loaded and read from a different URI */ function getOrFetchSchema(oMetaModel, mScope, sSchema, fnLog) { var oPromise, sUrl, aUrls, mUrls; /* * Include the schema (and all of its children) with namespace <code>sSchema</code> from * the given referenced scope. * * @param {object} mReferencedScope * The $metadata "JSON" */ function includeSchema(mReferencedScope) { var oElement, sKey; if (!(sSchema in mReferencedScope)) { fnLog(WARNING, sUrl, " does not contain ", sSchema); return; } fnLog(DEBUG, "Including ", sSchema, " from ", sUrl); for (sKey in mReferencedScope) { // $EntityContainer can be ignored; $Reference, $Version is handled above if (sKey[0] !== "$" && schema(sKey) === sSchema) { oElement = mReferencedScope[sKey]; mScope[sKey] = oElement; mergeAnnotations(oElement, mScope.$Annotations); } } } if (sSchema in mScope) { return mScope[sSchema]; } mUrls = oMetaModel.mSchema2MetadataUrl[sSchema]; if (mUrls) { aUrls = Object.keys(mUrls); if (aUrls.length > 1) { reportAndThrowError(oMetaModel, "A schema cannot span more than one document: " + "schema is referenced by following URLs: " + aUrls.join(", "), sSchema); } sUrl = aUrls[0]; mUrls[sUrl] = true; fnLog(DEBUG, "Namespace ", sSchema, " found in $Include of ", sUrl); oPromise = oMetaModel.mMetadataUrl2Promise[sUrl]; if (!oPromise) { fnLog(DEBUG, "Reading ", sUrl); oPromise = oMetaModel.mMetadataUrl2Promise[sUrl] = SyncPromise.resolve(oMetaModel.oRequestor.read(sUrl)) .then(oMetaModel.validate.bind(oMetaModel, sUrl)); } oPromise = oPromise.then(includeSchema); // BEWARE: oPromise may already be resolved, then includeSchema() is done now if (sSchema in mScope) { return mScope[sSchema]; } mScope[sSchema] = oPromise; return oPromise; } } /** * Checks that the term is the expected term and determines the qualifier. * * @param {string} sTerm * The term * @param {string} sExpectedTerm * The expected term * @returns {string|undefined} * The qualifier or <code>undefined</code>, if the term is not the expected term */ function getQualifier(sTerm, sExpectedTerm) { if (sTerm === sExpectedTerm) { return ""; } if (sTerm.startsWith(sExpectedTerm) && sTerm[sExpectedTerm.length] === "#" && !sTerm.includes("@", sExpectedTerm.length)) { return sTerm.slice(sExpectedTerm.length + 1); } } /** * Checks that the term is a ValueList or a ValueListMapping and determines the qualifier. * * @param {string} sTerm * The term * @returns {string|undefined} * The qualifier or <code>undefined</code>, if the term is not as expected */ function getValueListQualifier(sTerm) { var sQualifier = getQualifier(sTerm, sValueListMapping); return sQualifier !== undefined ? sQualifier : getQualifier(sTerm, sValueList); } /** * Tells whether the given name matches a parameter of at least one of the given overloads, or * is the special name "$ReturnType" and at least one of the given overloads has a return type. * * @param {string} sName * A path segment which maybe is a parameter name or the special name "$ReturnType" * @param {object[]} aOverloads * Operation overload(s) * @returns {boolean} * <code>true</code> iff at least one of the given overloads has a parameter with the given * name (incl. the special name "$ReturnType") */ function maybeParameter(sName, aOverloads) { return aOverloads.some(function (oOverload) { return sName === "$ReturnType" ? oOverload.$ReturnType : oOverload.$Parameter && oOverload.$Parameter.some(function (oParameter) { return oParameter.$Name === sName; }); }); } /** * Merges the given schema's annotations into the root scope's $Annotations. * * @param {object} oSchema * a schema; schema children are ignored because they do not contain $Annotations * @param {object} mAnnotations * the root scope's $Annotations * @param {boolean} [bPrivileged] * whether the schema has been loaded from a privileged source and thus may overwrite * existing annotations */ function mergeAnnotations(oSchema, mAnnotations, bPrivileged) { var sTarget; /* * "PUT" semantics on term/qualifier level, only privileged sources may overwrite. * * @param {object} oTarget * The target object (which is modified) * @param {object} oSource * The source object */ function extend(oTarget, oSource) { var sName; for (sName in oSource) { if (bPrivileged || !(sName in oTarget)) { oTarget[sName] = oSource[sName]; } } } for (sTarget in oSchema.$Annotations) { if (!(sTarget in mAnnotations)) { mAnnotations[sTarget] = {}; } extend(mAnnotations[sTarget], oSchema.$Annotations[sTarget]); } delete oSchema.$Annotations; } /** * Reports an error with the given message and details and throws it. * * @param {sap.ui.model.odata.v4.ODataMetaModel} oMetaModel * The OData metadata model * @param {string} sMessage * Error message * @param {string} sDetails * Error details * @throws {Error} */ function reportAndThrowError(oMetaModel, sMessage, sDetails) { var oError = new Error(sDetails + ": " + sMessage); oMetaModel.oModel.reportError(sMessage, sODataMetaModel, oError); throw oError; } /** * Returns the namespace of the given qualified name's schema, including the trailing dot. * * @param {string} sQualifiedName * A qualified name * @returns {string} * The schema's namespace */ function schema(sQualifiedName) { return sQualifiedName.slice(0, sQualifiedName.lastIndexOf(".") + 1); } //********************************************************************************************* // ODataMetaContextBinding //********************************************************************************************* /** * @class Context binding implementation for the OData metadata model. * * @extends sap.ui.model.ContextBinding * @private */ ODataMetaContextBinding = ContextBinding.extend("sap.ui.model.odata.v4.ODataMetaContextBinding", { constructor : function (oModel, sPath, oContext) { assert(!oContext || oContext.getModel() === oModel, "oContext must belong to this model"); ContextBinding.call(this, oModel, sPath, oContext); }, // @override // @see sap.ui.model.Binding#initialize initialize : function () { var oElementContext = this.oModel.createBindingContext(this.sPath, this.oContext); this.bInitial = false; // initialize() has been called if (oElementContext !== this.oElementContext) { this.oElementContext = oElementContext; this._fireChange(); } }, // @override // @see sap.ui.model.Binding#setContext setContext : function (oContext) { assert(!oContext || oContext.getModel() === this.oModel, "oContext must belong to this model"); if (oContext !== this.oContext) { this.oContext = oContext; if (!this.bInitial) { this.initialize(); } // else: do not cause implicit 1st initialize(), avoid _fireChange! } } }); //********************************************************************************************* // ODataMetaListBinding //********************************************************************************************* /** * @class List binding implementation for the OData metadata model which supports filtering on * the virtual property "@sapui.name" (which refers back to the name of the object in * question). * * Example: * <pre> * &lt;template:repeat list="{path : 'entityType>', filters : {path : '@sapui.name', operator : * 'StartsWith', value1 : 'com.sap.vocabularies.UI.v1.FieldGroup'}}" var="fieldGroup"> * </pre> * * @extends sap.ui.model.ClientListBinding * @private */ ODataMetaListBinding = ClientListBinding.extend("sap.ui.model.odata.v4.ODataMetaListBinding", { constructor : function () { ClientListBinding.apply(this, arguments); }, // @deprecated // @override // @see sap.ui.model.ListBinding#_fireFilter _fireFilter : function () { // do not fire an event as this function is deprecated }, // @deprecated // @override // @see sap.ui.model.ListBinding#_fireSort _fireSort : function () { // do not fire an event as this function is deprecated }, // @override // @see sap.ui.model.Binding#checkUpdate checkUpdate : function (bForceUpdate) { var iPreviousLength = this.oList.length; this.update(); // the data cannot change, only new items may be added due to lazy loading of references if (bForceUpdate || this.oList.length !== iPreviousLength) { this._fireChange({reason : ChangeReason.Change}); } }, /** * Returns the contexts that result from iterating over the binding's path/context. * @returns {sap.ui.base.SyncPromise} A promise that is resolved with an array of contexts * * @private */ fetchContexts : function () { var bIterateAnnotations, sResolvedPath = this.getResolvedPath(), that = this; if (!sResolvedPath) { return SyncPromise.resolve([]); } bIterateAnnotations = sResolvedPath.endsWith("@"); if (!bIterateAnnotations && !sResolvedPath.endsWith("/")) { sResolvedPath += "/"; } return this.oModel.fetchObject(sResolvedPath).then(function (oResult) { if (!oResult) { return []; } if (bIterateAnnotations) { // strip off the trailing "@" sResolvedPath = sResolvedPath.slice(0, -1); } return Object.keys(oResult).filter(function (sKey) { // always filter technical properties; // filter annotations iff not iterating them return sKey[0] !== "$" && bIterateAnnotations !== (sKey[0] !== "@"); }).map(function (sKey) { return new BaseContext(that.oModel, sResolvedPath + sKey); }); }); }, // @override // @see sap.ui.model.ListBinding#getContexts // the third parameter (iMaximumPrefetchSize) is ignored because the data is already // available completely getContexts : function (iStartIndex, iLength) { // extended change detection is ignored this.iCurrentStart = iStartIndex || 0; this.iCurrentLength = Math.min(iLength || Infinity, this.iLength - this.iCurrentStart); return this.getCurrentContexts(); }, // @override // @see sap.ui.model.ListBinding#getCurrentContexts getCurrentContexts : function () { var aContexts = [], i, n = this.iCurrentStart + this.iCurrentLength; for (i = this.iCurrentStart; i < n; i += 1) { aContexts.push(this.oList[this.aIndices[i]]); } if (this.oList.dataRequested) { aContexts.dataRequested = true; } return aContexts; }, /** * Updates the list and indices array from the given contexts. * @param {sap.ui.model.Context[]} aContexts The contexts * @private */ setContexts : function (aContexts) { this.oList = aContexts; this.updateIndices(); this.applyFilter(); this.applySort(); this.iLength = this._getLength(); }, /** * Updates the list and indices array. Fires a change event if the data was retrieved * asynchronously. * @private */ update : function () { var aContexts = [], oPromise = this.fetchContexts(), that = this; if (oPromise.isFulfilled()) { aContexts = oPromise.getResult(); } else { oPromise.then(function (aContexts) { that.setContexts(aContexts); that._fireChange({reason : ChangeReason.Change}); }); aContexts.dataRequested = true; } this.setContexts(aContexts); } }); //********************************************************************************************* // ODataMetaPropertyBinding //********************************************************************************************* /** * @class Property binding implementation for the OData metadata model. * * @extends sap.ui.model.PropertyBinding * @private */ ODataMetaPropertyBinding = PropertyBinding.extend("sap.ui.model.odata.v4.ODataMetaPropertyBinding", { constructor : function () { PropertyBinding.apply(this, arguments); this.vValue = undefined; }, // Updates the binding's value and sends a change event if the <code>bForceUpdate</code> // parameter is set to <code>true</code> or if the value has changed. // If the binding parameter <code>$$valueAsPromise</code> is <code>true</code> and the // value cannot be fetched synchronously then <code>getValue</code> returns a // <code>Promise</code> resolving with the value. After the value is resolved a second // change event is fired and <code>getValue</code> returns the value itself. // // @param {boolean} [bForceUpdate] // If <code>true</code>, the change event is always fired. // @param {sap.ui.model.ChangeReason} [sChangeReason=ChangeReason.Change] // The change reason for the change event // @override // @see sap.ui.model.Binding#checkUpdate checkUpdate : function (bForceUpdate, sChangeReason) { var oPromise, that = this; function setValue(vValue) { if (bForceUpdate || vValue !== that.vValue) { that.vValue = vValue; that._fireChange({ reason : sChangeReason || ChangeReason.Change }); } return vValue; } oPromise = this.oModel.fetchObject(this.sPath, this.oContext, this.mParameters) .then(setValue); if (this.mParameters && this.mParameters.$$valueAsPromise && oPromise.isPending()) { setValue(oPromise.unwrap()); } else if (oPromise.isRejected()) { oPromise.unwrap(); } }, // May return a <code>Promise</code> instead of the value if the binding parameter // <code>$$valueAsPromise</code> is <code>true</code> // @override // @see sap.ui.model.PropertyBinding#getValue getValue : function () { return this.vValue; }, // @override // @see sap.ui.model.Binding#setContext setContext : function (oContext) { if (this.oContext !== oContext) { this.oContext = oContext; if (this.bRelative) { this.checkUpdate(false, ChangeReason.Context); } } }, // @override // @see sap.ui.model.PropertyBinding#setValue setValue : function () { throw new Error("Unsupported operation: ODataMetaPropertyBinding#setValue"); } }); //********************************************************************************************* // ODataMetaModel //********************************************************************************************* /** * Do <strong>NOT</strong> call this private constructor, but rather use * {@link sap.ui.model.odata.v4.ODataModel#getMetaModel} instead. * * @param {sap.ui.model.odata.v4.lib._MetadataRequestor} oRequestor * The metadata requestor * @param {string} sUrl * The URL to the $metadata document of the service * @param {string|string[]} [vAnnotationUri] * The URL (or an array of URLs) from which the annotation metadata are loaded * Supported since 1.41.0 * @param {sap.ui.model.odata.v4.ODataModel} oModel * The model this meta model is related to * @param {boolean} [bSupportReferences=true] * Whether <code>&lt;edmx:Reference></code> and <code>&lt;edmx:Include></code> directives are * supported in order to load schemas on demand from other $metadata documents and include * them into the current service ("cross-service references"). * @param {string} [sLanguage] * The "sap-language" URL parameter */ function constructor(oRequestor, sUrl, vAnnotationUri, oModel, bSupportReferences, sLanguage) { MetaModel.call(this); this.aAnnotationUris = vAnnotationUri && !Array.isArray(vAnnotationUri) ? [vAnnotationUri] : vAnnotationUri; this.sDefaultBindingMode = BindingMode.OneTime; this.mETags = {}; this.sLanguage = sLanguage; this.oLastModified = new Date(0); this.oMetadataPromise = null; this.oModel = oModel; this.mMetadataUrl2Promise = {}; this.oRequestor = oRequestor; // maps the schema name to a map containing the URL references for the schema as key // and a boolean value whether the schema has been read already as value; the URL // reference is used by _MetadataRequestor#read() // Example: // mSchema2MetadataUrl = { // "A." : {"/A/$metadata" : false}, // namespace not yet read // // multiple references are ok as long as they are not read // "A.A." : {"/A/$metadata" : false, "/A/V2/$metadata" : false}, // "B." : {"/B/$metadata" : true} // namespace already read // } this.mSchema2MetadataUrl = {}; this.mSupportedBindingModes = {OneTime : true, OneWay : true}; this.bSupportReferences = bSupportReferences !== false; // default is true // ClientListBinding#filter calls checkFilterOperation on the model; ClientModel does // not support "All" and "Any" filters this.mUnsupportedFilterOperators = {All : true, Any : true}; this.sUrl = sUrl; } /** * Indicates that the property bindings of this model support the binding parameter * <code>$$valueAsPromise</code> which allows to return the value as a <code>Promise</code> if * the value cannot be fetched synchronously. * * @private */ ODataMetaModel.prototype.$$valueAsPromise = true; /** * Merges <code>$Annotations</code> from the given $metadata and additional annotation files * into the root scope as a new map of all annotations, called <code>$Annotations</code>. * * @param {object} mScope * The $metadata "JSON" of the root service * @param {object[]} aAnnotationFiles * The metadata "JSON" of the additional annotation files * @throws {Error} * If metadata cannot be merged or if the schema has already been loaded from a different URI * * @private */ ODataMetaModel.prototype._mergeAnnotations = function (mScope, aAnnotationFiles) { var that = this; this.validate(this.sUrl, mScope); // merge $Annotations from all schemas at root scope mScope.$Annotations = {}; Object.keys(mScope).forEach(function (sElement) { if (mScope[sElement].$kind === "Schema") { addUrlForSchema(that, sElement, that.sUrl); mergeAnnotations(mScope[sElement], mScope.$Annotations); } }); // merge annotation files into root scope aAnnotationFiles.forEach(function (mAnnotationScope, i) { var oElement, sQualifiedName; that.validate(that.aAnnotationUris[i], mAnnotationScope); for (sQualifiedName in mAnnotationScope) { if (sQualifiedName[0] !== "$") { if (sQualifiedName in mScope) { reportAndThrowError(that, "A schema cannot span more than one document: " + sQualifiedName, that.aAnnotationUris[i]); } oElement = mAnnotationScope[sQualifiedName]; mScope[sQualifiedName] = oElement; if (oElement.$kind === "Schema") { addUrlForSchema(that, sQualifiedName, that.aAnnotationUris[i]); mergeAnnotations(oElement, mScope.$Annotations, true); } } } }); }; /** * 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 ODataMetaModel.prototype.attachEvent = function (sEventId, _oData, _fnFunction, _oListener) { if (!(sEventId in mSupportedEvents)) { throw new Error("Unsupported event '" + sEventId + "': v4.ODataMetaModel#attachEvent"); } return MetaModel.prototype.attachEvent.apply(this, arguments); }; /** * See <code>sap.ui.model.Model#bindContext</code> * * @param {string} sPath * @param {sap.ui.model.Context} [oContext] * * @returns {sap.ui.model.ContextBinding} * * @public * @see sap.ui.model.Model#bindContext * @since 1.37.0 */ // @override sap.ui.model.Model#bindContext ODataMetaModel.prototype.bindContext = function (sPath, oContext) { return new ODataMetaContextBinding(this, sPath, oContext); }; /** * Creates a list binding for this metadata model which iterates content from the given path * (relative to the given context), sorted and filtered as indicated. * * By default, OData names are iterated and a trailing slash is implicitly added to the path * (see {@link #requestObject} for the effects this has); technical properties and inline * annotations are filtered out. * * A path which ends with an "@" segment can be used to iterate all inline or external * targeting annotations; no trailing slash is added implicitly; technical properties and OData * names are filtered out. * * @param {string} sPath * A relative or absolute path within the metadata model, for example "/EMPLOYEES" * @param {sap.ui.model.Context} [oContext] * The context to be used as a starting point in case of a relative path * @param {sap.ui.model.Sorter|sap.ui.model.Sorter[]} [aSorters] * Initial sort order, see {@link sap.ui.model.ListBinding#sort} * @param {sap.ui.model.Filter|sap.ui.model.Filter[]} [aFilters] * Initial application filter(s), see {@link sap.ui.model.ListBinding#filter}; filters with * filter operators "All" or "Any" are not supported * @returns {sap.ui.model.ListBinding} * A list binding for this metadata model * * @public * @see sap.ui.model.Model#bindList * @see #requestObject * @see sap.ui.model.FilterOperator * @since 1.37.0 */ // @override sap.ui.model.Model#bindList ODataMetaModel.prototype.bindList = function (sPath, oContext, aSorters, aFilters) { return new ODataMetaListBinding(this, sPath, oContext, aSorters, aFilters); }; /** * Creates a property binding for this metadata model which refers to the content from the * given path (relative to the given context). * * @param {string} sPath * A relative or absolute path within the metadata model, for example "/EMPLOYEES/ENTRYDATE" * @param {sap.ui.model.Context} [oContext] * The context to be used as a starting point in case of a relative path * @param {object} [mParameters] * Optional binding parameters that are passed to {@link #getObject} to compute the binding's * value; if they are given, <code>oContext</code> cannot be omitted * @param {boolean} [mParameters.$$valueAsPromise] * Whether {@link sap.ui.model.PropertyBinding#getValue} may return a <code>Promise</code> * resolving with the value (since 1.57.0) * @param {object} [mParameters.scope] * Optional scope for lookup of aliases for computed annotations (since 1.43.0) * @returns {sap.ui.model.PropertyBinding} * A property binding for this metadata model * * @public * @see sap.ui.model.Model#bindProperty * @since 1.37.0 */ // @override sap.ui.model.Model#bindProperty ODataMetaModel.prototype.bindProperty = function (sPath, oContext, mParameters) { return new ODataMetaPropertyBinding(this, sPath, oContext, mParameters); }; /** * Method not supported * * @param {string} _sPath * @param {sap.ui.model.Context} [_oContext] * @param {sap.ui.model.Filter[]} [_aFilters] * @param {object} [_mParameters] * @param {sap.ui.model.Sorter[]} [_aSorters] * @returns {sap.ui.model.TreeBinding} * @throws {Error} * * @public * @see sap.ui.model.Model#bindTree * @since 1.37.0 */ // @override sap.ui.model.Model#bindTree ODataMetaModel.prototype.bindTree = function (_sPath, _oContext, _aFilters, _mParameters, _aSorters) { throw new Error("Unsupported operation: v4.ODataMetaModel#bindTree"); }; /** * Returns a promise for an absolute data binding path of a "4.3.1 Canonical URL" for the given * context. * * @param {sap.ui.model.odata.v4.Context} oContext * OData V4 context object for which the canonical path is requested; it must point to an * entity * @returns {sap.ui.base.SyncPromise} * A promise which is resolved with the canonical path (for example "/EMPLOYEES('1')") in * case of success; it is rejected if the requested metadata cannot be loaded, if the context * path does not point to an entity, if the entity is transient, or if required key properties * are missing * * @private */ ODataMetaModel.prototype.fetchCanonicalPath = function (oContext) { return this.fetchUpdateData("", oContext).then(function (oResult) { if (!oResult.editUrl) { throw new Error(oContext.getPath() + ": No canonical path for transient entity"); } if (oResult.propertyPath) { throw new Error("Context " + oContext.getPath() + " does not point to an entity. It should be " + oResult.entityPath); } return "/" + oResult.editUrl; }); }; /** * Requests the metadata. * * @returns {sap.ui.base.SyncPromise} * A promise which is resolved with the requested metadata as soon as it is available * * @private */ ODataMetaModel.prototype.fetchData = function () { return this.fetchEntityContainer().then(function (mScope) { return JSON.parse(JSON.stringify(mScope)); }); }; /** * Requests the single entity container for this metadata model's service by reading the * $metadata document via the metadata requestor. The resulting $metadata "JSON" object is a map * of qualified names to their corresponding metadata, with the special key "$EntityContainer" * mapped to the entity container's qualified name as a starting point. * * @param {boolean} [bPrefetch] * Whether to just read the $metadata document and annotations, but not yet convert them from * XML to JSON; this is useful at most once in an early call that precedes all other normal * calls and ignored after the first call without this. * @returns {sap.ui.base.SyncPromise} * A promise which is resolved with the $metadata "JSON" object as soon as the entity * container is fully available, or rejected with an error. In case of * <code>bPrefetch</code> in an early call, <code>null</code> is returned. * * @private */ ODataMetaModel.prototype.fetchEntityContainer = function (bPrefetch) { var aPromises, that = this; if (!this.oMetadataPromise) { aPromises = [SyncPromise.resolve(this.oRequestor.read(this.sUrl, false, bPrefetch))]; if (this.aAnnotationUris) { this.aAnnotationUris.forEach(function (sAnnotationUri) { aPromises.push(SyncPromise.resolve( that.oRequestor.read(sAnnotationUri, true, bPrefetch))); }); } if (!bPrefetch) { this.oMetadataPromise = SyncPromise.all(aPromises).then(function (aMetadata) { var mScope = aMetadata[0]; that._mergeAnnotations(mScope, aMetadata.slice(1)); return mScope; }); } } return this.oMetadataPromise; }; /** * @param {string} sPath * A relative or absolute path within the metadata model, for example "/EMPLOYEES/ENTRYDATE" * @param {sap.ui.model.Context} [oContext] * The context to be used as a starting point in case of a relative path * @param {object} [mParameters] * Optional (binding) parameters; if they are given, <code>oContext</code> cannot be omitted * @param {boolean} [mParameters.$$valueAsPromise] * Whether a computed annotation may return a <code>Promise</code> resolving with its value * (since 1.57.0) * @param {object} [mParameters.scope] * Optional scope for lookup of aliases for computed annotations (since 1.43.0) * @returns {sap.ui.base.SyncPromise} * A promise which is resolved with the requested metadata object as soon as it is available; * it is rejected if the requested metadata cannot be loaded * * @private * @see #requestObject */ ODataMetaModel.prototype.fetchObject = function (sPath, oContext, mParameters) { var sResolvedPath = this.resolve(sPath, oContext), that = this; if (!sResolvedPath) { Log.error("Invalid relative path w/o context", sPath, sODataMetaModel); return SyncPromise.resolve(null); } return this.fetchEntityContainer().then(function (mScope) { // binding parameter's type name ({string}) for overloading of bound operations // or UNBOUND ({object}) for unbound operations called via an import var vBindingParameterType, // the entity set or singleton targeted by the current annotation, if any oEntitySetOrSingleton, bInsideAnnotation, // inside an annotation, invalid names are OK vLocation, // {string[]|string} location of indirection sName, // what "@sapui.name" refers to: OData or annotation name bODataMode, // OData navigation mode with scope lookup etc. // parent for next "17.2 SimpleIdentifier"... // (normally the schema child containing the current object) oSchemaChild, // ...as object sSchemaChildName, // ...as qualified name // annotation target pointing to current object, or undefined // (schema child's qualified name plus optional segments) sTarget, vResult; // current object /* * Handles annotation at operation or parameter, taking care of individual versus all * overloads. * * @param {string} sSegment * The current <code>sSegment</code> of <code>step</code> * @param {string} [sTerm=sSegment] * The term * @param {string} [sSuffix=""] * A target suffix to address a parameter * @returns {boolean} * Whether further steps are needed */ function annotationAtOperationOrParameter(sSegment, sTerm, sSuffix) { var mAnnotationsXAllOverloads, iIndexOfAtAt, sIndividualOverloadTarget, aOverloads, sSignature = ""; if (sTerm) { // split trailing computed annotation iIndexOfAtAt = sTerm.indexOf("@@"); if (iIndexOfAtAt > 0) { sTerm = sTerm.slice(0, iIndexOfAtAt); } // Note: The case that sTerm includes @sapui.name need not be handled here. // sTerm is used below only to determine the correct annotation target, but that // does not matter because the term's name can be determined nevertheless. } else { sTerm = sSegment; } sSuffix = sSuffix || ""; if (vBindingParameterType) { oSchemaChild = aOverloads = vResult.filter(isRightOverload); if (aOverloads.length !== 1) { return log(WARNING, "Expected a single overload, but found " + aOverloads.length); } if (vBindingParameterType !== UNBOUND) { sSignature = aOverloads[0].$Parameter[0].$isCollection ? "Collection(" + vBindingParameterType + ")" : vBindingParameterType; } sIndividualOverloadTarget = sTarget + "(" + sSignature + ")" + sSuffix; if (mScope.$Annotations[sIndividualOverloadTarget]) { if (sTerm === "@") { vResult = mScope.$Annotations[sIndividualOverloadTarget]; mAnnotationsXAllOverloads = mScope.$Annotations[sTarget + sSuffix]; if (mAnnotationsXAllOverloads) { vResult = Object.assign({}, mAnnotationsXAllOverloads, vResult); } // sTarget does not matter because no further steps follow return false; // no further steps must happen } if (sTerm in mScope.$Annotations[sIndividualOverloadTarget]) { // "external targeting of individual operation overload" sTarget = sIndividualOverloadTarget; vResult = mScope; // see below return true; } } } // "the annotation applies to all overloads of the action or function or all // parameters of that name across all overloads" [OData-Part3] sTarget += sSuffix; // any object (no array!) should do here to skip repeated handling of overloads vResult = mScope; return true; } /* * Calls a computed annotation according to the given segment which was found at the * given path; changes <code>vResult</code> accordingly. * * @param {string} sSegment * Contains the name of the computed annotation as "@@..." * @param {string} sPath * Path where the segment was found * @returns {boolean} * <code>true</code> */ function computedAnnotation(sSegment, sPath) { var fnAnnotation, aArguments, iLeftParenthesis, iThirdAt = sSegment.indexOf("@", 2); if (iThirdAt > -1) { return log(WARNING, "Unsupported path after ", sSegment.slice(0, iThirdAt)); } sSegment = sSegment.slice(2); iLeftParenthesis = sSegment.indexOf("("); if (iLeftParenthesis > 0) { if (!sSegment.endsWith(")")) { return log(WARNING, "Expected ')' instead of '", sSegment.slice(-1), "'"); } try { aArguments = JSTokenizer.parseJS("[" + sSegment.slice(iLeftParenthesis + 1, -1) // restore *all* braces .replace(rLeftBraces, "{").replace(rRightBraces, "}") + "]"); } catch (e) { // parse error return log(WARNING, e.message, ": ", e.text.slice(1, e.at), "<--", e.text.slice(e.at, -1)); } sSegment = sSegment.slice(0, iLeftParenthesis); } fnAnnotation = sSegment[0] === "." ? ObjectPath.get(sSegment.slice(1), mParameters.scope) : mParameters && ObjectPath.get(sSegment, mParameters.scope) || (sSegment === "requestCurrencyCodes" || sSegment === "requestUnitsOfMeasure" ? that[sSegment].bind(that) : ObjectPath.get(sSegment)); if (typeof fnAnnotation !== "function") { // Note: "varargs" syntax does not help because Array#join ignores undefined return log(WARNING, sSegment, " is not a function but: " + fnAnnotation); } try { vResult = fnAnnotation(vResult, { $$valueAsPromise : mParameters && mParameters.$$valueAsPromise, arguments : aArguments, context : new BaseContext(that, sPath), schemaChildName : sSchemaChildName, // Note: length === 1 implies Array.isArray(oSchemaChild) overload : oSchemaChild.length === 1 ? oSchemaChild[0] : undefined }); } catch (e) { log(WARNING, "Error calling ", sSegment, ": ", e); } return true; } /* * Tells whether the given segment matches a parameter of the given overload, or is the * special name "$ReturnType" and the given overload has a return type; changes * <code>vResult</code> etc. accordingly. * * @param {string} sSegment * A segment (may be empty) * @param {object} oOverload * A single operation overload * @returns {boolean} * <code>true</code> iff the given overload has a parameter with the given name * (incl. the special name "$ReturnType") */ function isParameter(sSegment, oOverload) { var aMatches; if (sSegment === "$ReturnType") { if (oOverload.$ReturnType) { vResult = oOverload.$ReturnType; return true; } } else if (sSegment && oOverload.$Parameter) { aMatches = oOverload.$Parameter.filter(function (oParameter) { return oParameter.$Name === sSegment; }); if (aMatches.length) { // there can be at most one match // Note: annotations at operation or parameter are handled before this // method is called; @see annotationAtOperationOrParameter vResult = aMatches[0]; return true; } } return false; } /* * Tells whether the given overload is the right one. * * @param {object} oOverload * A single operation overload * @returns {boolean} * <code>true</code> iff the given overload is an operation with the appropriate * binding parameter (bound and unbound cases). */ function isRightOverload(oOverload) { return !oOverload.$IsBound && vBindingParameterType === UNBOUND || oOverload.$IsBound && vBindingParameterType === oOverload.$Parameter[0].$Type; } /* * Outputs a log message for the given level. Leads to an <code>undefined</code> result * in case of a WARNING. * * @param {sap.base.Log.Level} iLevel * A log level, either DEBUG or WARNING * @param {...string} aTexts * The main text of the message is constructed from the rest of the arguments by * joining them * @returns {boolean} * <code>false</code> */ function log(iLevel) { var sLocation; if (Log.isLoggable(iLevel, sODataMetaModel)) { sLocation = Array.isArray(vLocation) ? vLocation.join("/") : vLocation; Log[iLevel === DEBUG ? "debug" : "warning"]( Array.prototype.slice.call(arguments, 1).join("") + (sLocation ? " at /" + sLocation : ""), sResolvedPath, sODataMetaModel); } if (iLevel === WARNING) { vResult = undefined; } return false; } /* * Looks up the given qualified name in the global scope. * * @param {string} sQualifiedName * A qualified name * @param {string} [sPropertyName] * Where the qualified name was found * @returns {boolean} * Whether to continue after scope lookup */ function scopeLookup(sQualifiedName, sPropertyName) { var sSchema; /* * Sets <code>vLocation</code> and delegates to {@link log}. */ function logWithLocation() { vLocation = vLocation || sTarget && sPropertyName && sTarget + "/" + sPropertyName; return log.apply(this, arguments); } vBindingParameterType = vResult && vResult.$Type || vBindingParameterType; if (that.bSupportReferences && !(sQualifiedName in mScope)) { // unknown qualified name: maybe schema is referenced and can be included? sSchema = schema(sQualifiedName); vResult = getOrFetchSchema(that, mScope, sSchema, logWithLocation); } if (sQualifiedName in mScope) { sTarget = sName = sSchemaChildName = sQualifiedName; vResult = oSchemaChild = mScope[sSchemaChildName]; if (!SyncPromise.isThenable(vResult)) { return true; // qualified name found, steps may continue } } if (SyncPromise.isThenable(vResult) && vResult.isPending()) { // load on demand still pending (else it must be rejected at this point) return logWithLocation(DEBUG, "Waiting for ", sSchema); } return logWithLocation(WARNING, "Unknown qualified name ", sQualifiedName); } /* * Takes one step according to the given segment, starting at the current * <code>vResult</code> and changing that. * * @param {string} sSegment * Current segment * @param {number} i * Current segment's index * @param {string[]} aSegments * All segments * @returns {boolean} * Whether to continue after this step */ function step(sSegment, i, aSegments) { var iIndexOfAt, bResultIsObject, bSplitSegment; if (sSegment === "$Annotations") { return log(WARNING, "Invalid segment: $Annotations"); } sSegment = sSegment.replaceAll("%2F", "/"); if (i && typeof vResult === "object" && sSegment in vResult) { // fast path for pure "JSON" drill-down, but this cannot replace scopeLookup()! if (sSegment[0] === "$" || rNumber.test(sSegment)) { bODataMode = false; // technical property, switch to pure "JSON" drill-down } } else { // split trailing computed annotation, @sapui.name, or annotation iIndexOfAt = sSegment.indexOf("@@"); if (iIndexOfAt < 0) { if (sSegment.endsWith("@sapui.name")) { iIndexOfAt = sSegment.length - 11; } else { iIndexOfAt = sSegment.indexOf("@"); } } if (iIndexOfAt > 0) { // <17.2 SimpleIdentifier|17.3 QualifiedName>@<annotation[@annotation]> // Note: only the 1st annotation may use external targeting, the rest is // pure "JSON" drill-down (except for computed annotations/"@sapui.name")! if (!step(sSegment.slice(0, iIndexOfAt), i, aSegments)) { return false; } sSegment = sSegment.slice(iIndexOfAt); bSplitSegment = true; if (vResult && (vResult.$kind === "EntitySet" || vResult.$kind === "Singleton")) { oEntitySetOrSingleton = vResult; } } if (typeof vResult === "string" && !(bSplitSegment && (sSegment === "@sapui.name" || sSegment[1] === "@")) && !(bODataMode && oSchemaChild && oSchemaChild.$kind === "EnumType") // indirection: treat string content as a meta model path unless followed by // a computed annotation or if it is an (Edm.Int64) enum member value && !steps(vResult, aSegments.slice(0, i))) { return false; } if (bODataMode) { if (sSegment[0] === "$" && sSegment !== "$Parameter" && sSegment !== "$ReturnType" || rNumber.test(sSegment)) { // technical property, switch to pure "JSON" drill-down bODataMode = false; } else { bResultIsObject = typeof vResult === "object"; if (bSplitSegment) { // no special preparations needed, but handle overloads below! } else if (sSegment[0] !== "@" && sSegment.includes(".", 1)) { // "17.3 QualifiedName": scope lookup return scopeLookup(sSegment); } else if (bResultIsObject && "$Type" in vResult) { // implicit $Type insertion, e.g. at (navigation) property if (!scopeLookup(vResult.$Type, "$Type")) { return false; } } else if (bResultIsObject && "$Action" in vResult) { // implicit $Action insertion at action import if (!scopeLookup(vResult.$Action, "$Action")) { return false; } vBindingParameterType = UNBOUND; } else if (bResultIsObject && "$Function" in vResult) { // implicit $Function insertion at function import if (!scopeLookup(vResult.$Function, "$Function")) { return false; } vBindingParameterType = UNBOUND; } else if (!i) { // "17.2 SimpleIdentifier" (or placeholder): // lookup inside schema child (which is determined lazily) sTarget = sName = sSchemaChildName = sSchemaChildName || mScope.$EntityContainer; vResult = oSchemaChild = oSchemaChild || mScope[sSchemaChildName]; if (Array.isArray(vResult)) { if (vBindingParameterType) { vResult = vResult.filter(isRightOverload); } if (isParameter(sSegment, vResult[0])) { // path evaluation relative to an operation overload // @see [OData-CSDL-JSON-v4.01] "14.4.1.2 Path Evaluatio