@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,417 lines (1,320 loc) • 139 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
//Provides 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><edmx:Reference></code> and <code><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>
* <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><edmx:Reference></code> and <code><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