@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,268 lines (1,177 loc) • 52.1 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.
*/
/*eslint-disable max-len */
sap.ui.define([
"./_ODataMetaModelUtils",
"sap/base/Log",
"sap/base/util/extend",
"sap/base/util/isEmptyObject",
"sap/base/util/UriParameters",
"sap/ui/base/BindingParser",
"sap/ui/base/ManagedObject",
"sap/ui/base/SyncPromise",
"sap/ui/model/_Helper",
"sap/ui/model/BindingMode",
"sap/ui/model/ClientContextBinding",
"sap/ui/model/Context",
"sap/ui/model/FilterProcessor",
"sap/ui/model/MetaModel",
"sap/ui/model/json/JSONListBinding",
"sap/ui/model/json/JSONModel",
"sap/ui/model/json/JSONPropertyBinding",
"sap/ui/model/json/JSONTreeBinding",
"sap/ui/performance/Measurement"
], function (Utils, Log, extend, isEmptyObject, UriParameters, BindingParser, ManagedObject,
SyncPromise, _Helper, BindingMode, ClientContextBinding, Context, FilterProcessor, MetaModel,
JSONListBinding, JSONModel, JSONPropertyBinding, JSONTreeBinding, Measurement) {
"use strict";
var // maps the metadata URL with query parameters concatenated with the code list collection
// path (e.g. /foo/bar/$metadata#SAP__Currencies) to a SyncPromise resolving with the code
// list customizing as needed by the OData type
mCodeListUrl2Promise = new Map(),
sODataMetaModel = "sap.ui.model.odata.ODataMetaModel",
aPerformanceCategories = [sODataMetaModel],
sPerformanceLoad = sODataMetaModel + "/load",
// path to a type's property e.g. ("/dataServices/schema/<i>/entityType/<j>/property/<k>")
rPropertyPath =
/^((\/dataServices\/schema\/\d+)\/(?:complexType|entityType)\/\d+)\/property\/\d+$/;
/**
* @class List binding implementation for the OData meta 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.json.JSONListBinding
* @private
*/
var ODataMetaListBinding = JSONListBinding.extend("sap.ui.model.odata.ODataMetaListBinding"),
Resolver = ManagedObject.extend("sap.ui.model.odata._resolver", {
metadata : {
properties : {
any : "any"
}
}
});
ODataMetaListBinding.prototype.applyFilter = function () {
var that = this,
oCombinedFilter = FilterProcessor.combineFilters(this.aFilters, this.aApplicationFilters);
this.aIndices = FilterProcessor.apply(this.aIndices, oCombinedFilter, function (vRef, sPath) {
return sPath === "@sapui.name"
? vRef
: that.oModel.getProperty(sPath, that.oList[vRef]);
}, this.mNormalizeCache);
this.iLength = this.aIndices.length;
};
/**
* DO NOT CALL this private constructor for a new <code>ODataMetaModel</code>,
* but rather use {@link sap.ui.model.odata.v2.ODataModel#getMetaModel getMetaModel} instead!
*
* @param {sap.ui.model.odata.ODataMetadata} oMetadata
* the OData model's metadata object
* @param {sap.ui.model.odata.ODataAnnotations} [oAnnotations]
* the OData model's annotations object
* @param {sap.ui.model.odata.v2.ODataModel} oDataModel
* the data model instance
*
* @class Implementation of an OData meta model which offers a unified access to both OData V2
* metadata and V4 annotations. It uses the existing {@link sap.ui.model.odata.ODataMetadata}
* as a foundation and merges V4 annotations from the existing
* {@link sap.ui.model.odata.ODataAnnotations} directly into the corresponding model element.
*
* This model is not prepared to be inherited from.
*
* Also, annotations from the "http://www.sap.com/Protocols/SAPData" namespace are lifted up
* from the <code>extensions</code> array and transformed from objects into simple properties
* with an "sap:" prefix for their name. Note that this happens in addition, thus the
* following example shows both representations. This way, such annotations can be addressed
* via a simple relative path instead of searching an array.
* <pre>
{
"name" : "BusinessPartnerID",
"extensions" : [{
"name" : "label",
"value" : "Bus. Part. ID",
"namespace" : "http://www.sap.com/Protocols/SAPData"
}],
"sap:label" : "Bus. Part. ID"
}
* </pre>
*
* As of 1.29.0, the corresponding vocabulary-based annotations for the following
* "<a href="http://www.sap.com/Protocols/SAPData">SAP Annotations for OData Version 2.0</a>"
* are added, if they are not yet defined in the V4 annotations:
* <ul>
* <li><code>label</code>;</li>
* <li><code>schema-version</code> (since 1.53.0) on schemas;</li>
* <li><code>creatable</code>, <code>deletable</code>, <code>deletable-path</code>,
* <code>pageable</code>, <code>requires-filter</code>, <code>searchable</code>,
* <code>topable</code>, <code>updatable</code> and <code>updatable-path</code> on entity sets;
* </li>
* <li><code>creatable</code> (since 1.41.0), <code>creatable-path</code> (since 1.41.0) and
* <code>filterable</code> (since 1.39.0) on navigation properties;</li>
* <li><code>aggregation-role</code> ("dimension" and "measure", both since 1.45.0),
* <code>creatable</code>, <code>display-format</code> ("UpperCase" and "NonNegative"),
* <code>field-control</code>, <code>filterable</code>, <code>filter-restriction</code>,
* <code>heading</code>, <code>precision</code>, <code>quickinfo</code>,
* <code>required-in-filter</code>, <code>sortable</code>, <code>text</code>, <code>unit</code>,
* <code>updatable</code> and <code>visible</code> on properties;</li>
* <li><code>semantics</code>; the following values are supported:
* <ul>
* <li>"bday", "city", "country", "email" (including support for types, for example
* "email;type=home,pref"), "familyname", "givenname", "honorific", "middlename", "name",
* "nickname", "note", "org", "org-unit", "org-role", "photo", "pobox", "region", "street",
* "suffix", "tel" (including support for types, for example "tel;type=cell,pref"), "title" and
* "zip" (mapped to V4 annotation <code>com.sap.vocabularies.Communication.v1.Contact</code>);
* </li>
* <li>"class", "dtend", "dtstart", "duration", "fbtype", "location", "status", "transp" and
* "wholeday" (mapped to V4 annotation
* <code>com.sap.vocabularies.Communication.v1.Event</code>);</li>
* <li>"body", "from", "received", "sender" and "subject" (mapped to V4 annotation
* <code>com.sap.vocabularies.Communication.v1.Message</code>);</li>
* <li>"completed", "due", "percent-complete" and "priority" (mapped to V4 annotation
* <code>com.sap.vocabularies.Communication.v1.Task</code>);</li>
* <li>"fiscalyear", "fiscalyearperiod" (mapped to the corresponding V4 annotation
* <code>com.sap.vocabularies.Common.v1.IsFiscal(Year|YearPeriod)</code>);</li>
* <li>"year", "yearmonth", "yearmonthday", "yearquarter", "yearweek" (mapped to the
* corresponding V4 annotation
* <code>com.sap.vocabularies.Common.v1.IsCalendar(Year|YearMonth|Date|YearQuarter|YearWeek)</code>);
* </li>
* <li>"url" (mapped to V4 annotation <code>Org.OData.Core.V1.IsURL"</code>).</li>
* </ul>
* </ul>
* For example:
* <pre>
{
"name" : "BusinessPartnerID",
...
"sap:label" : "Bus. Part. ID",
"com.sap.vocabularies.Common.v1.Label" : {
"String" : "Bus. Part. ID"
}
}
* </pre>
* <b>Note:</b> Annotation terms are not merged, but replaced as a whole ("PUT" semantics). That
* means, if you have, for example, an OData V2 annotation <code>sap:sortable=false</code> at a
* property <code>PropA</code>, the corresponding OData V4 annotation is added to each entity
* set to which this property belongs:
* <pre>
"Org.OData.Capabilities.V1.SortRestrictions": {
"NonSortableProperties" : [
{"PropertyPath" : "BusinessPartnerID"}
]
}
* </pre>
* If the same term <code>"Org.OData.Capabilities.V1.SortRestrictions"</code> targeting one of
* these entity sets is also contained in an annotation file, the complete OData V4 annotation
* converted from the OData V2 annotation is replaced by the one contained in the annotation
* file for the specified target. Converted annotations never use a qualifier and are only
* overwritten by the same annotation term without a qualifier.
*
* This model is read-only and thus only supports
* {@link sap.ui.model.BindingMode.OneTime OneTime} binding mode. No events
* ({@link sap.ui.model.Model#event:parseError parseError},
* {@link sap.ui.model.Model#event:requestCompleted requestCompleted},
* {@link sap.ui.model.Model#event:requestFailed requestFailed},
* {@link sap.ui.model.Model#event:requestSent requestSent}) are fired!
*
* Within the meta model, the objects are arranged in arrays.
* <code>/dataServices/schema</code>, for example, is an array of schemas where each schema has
* an <code>entityType</code> property with an array of entity types, and so on. So,
* <code>/dataServices/schema/0/entityType/16</code> can be the path to the entity type with
* name "Order" in the schema with namespace "MySchema". However, these paths are not stable:
* If an entity type with lower index is removed from the schema, the path to
* <code>Order</code> changes to <code>/dataServices/schema/0/entityType/15</code>.
*
* To avoid problems with changing indexes, {@link sap.ui.model.Model#getObject getObject} and
* {@link sap.ui.model.Model#getProperty getProperty} support XPath-like queries for the
* indexes (since 1.29.1). Each index can be replaced by a query in square brackets. You can,
* for example, address the schema using the path
* <code>/dataServices/schema/[${namespace}==='MySchema']</code> or the entity using
* <code>/dataServices/schema/[${namespace}==='MySchema']/entityType/[${name}==='Order']</code>.
*
* The syntax inside the square brackets is the same as in expression binding. The query is
* executed for each object in the array until the result is true (truthy) for the first time.
* This object is then chosen.
*
* <b>BEWARE:</b> Access to this OData meta model will fail before the promise returned by
* {@link #loaded loaded} has been resolved!
*
* @author SAP SE
* @version 1.111.5
* @alias sap.ui.model.odata.ODataMetaModel
* @extends sap.ui.model.MetaModel
* @public
* @since 1.27.0
*/
var ODataMetaModel = MetaModel.extend("sap.ui.model.odata.ODataMetaModel", {
constructor : function (oMetadata, oAnnotations, oDataModel) {
var oAnnotationsLoadedPromise = oDataModel.annotationsLoaded(),
that = this;
function load() {
var oData;
if (that.bDestroyed) {
throw new Error("Meta model already destroyed");
}
Measurement.average(sPerformanceLoad, "", aPerformanceCategories);
oData = JSON.parse(JSON.stringify(oMetadata.getServiceMetadata()));
that.oModel = new JSONModel(oData);
that.oModel.setDefaultBindingMode(that.sDefaultBindingMode);
Utils.merge(oAnnotations ? oAnnotations.getData() : {}, oData, that);
Measurement.end(sPerformanceLoad);
}
MetaModel.apply(this); // no arguments to pass!
this.oModel = null; // not yet available!
// map path of property to promise for loading its value list
this.mContext2Promise = {};
this.sDefaultBindingMode = BindingMode.OneTime;
this.oLoadedPromise = oAnnotationsLoadedPromise
? oAnnotationsLoadedPromise.then(load)
: new Promise(function (fnResolve, fnReject) {
load();
fnResolve();
}); // call load() synchronously!
this.oLoadedPromiseSync = SyncPromise.resolve(this.oLoadedPromise);
this.oMetadata = oMetadata;
this.oDataModel = oDataModel;
this.mQueryCache = {};
// map qualified property name to internal "promise interface" for request bundling
this.mQName2PendingRequest = {};
this.oResolver = undefined;
this.mSupportedBindingModes = {"OneTime" : true};
}
});
/**
* Returns the value of the object or property inside this model's data which can be reached,
* starting at the given context, by following the given path.
*
* @param {string} sPath
* a relative or absolute path
* @param {object|sap.ui.model.Context} [oContext]
* the context to be used as a starting point in case of a relative path
* @returns {any}
* the value of the object or property or <code>null</code> in case a relative path without
* a context is given
* @private
*/
ODataMetaModel.prototype._getObject = function (sPath, oContext) {
var oBaseNode = oContext,
oBinding,
sCacheKey,
i,
iEnd,
oNode,
vPart,
sProcessedPath,
sResolvedPath = sPath || "",
oResult;
if (!oContext || oContext instanceof Context) {
sResolvedPath = this.resolve(sPath || "", oContext);
if (!sResolvedPath) {
Log.error("Invalid relative path w/o context", sPath,
sODataMetaModel);
return null;
}
}
if (sResolvedPath.charAt(0) === "/") {
oBaseNode = this.oModel._getObject("/");
sResolvedPath = sResolvedPath.slice(1);
}
sProcessedPath = "/";
oNode = oBaseNode;
while (sResolvedPath) {
vPart = undefined;
oBinding = undefined;
if (sResolvedPath.charAt(0) === '[') {
try {
oResult = BindingParser.parseExpression(sResolvedPath, 1);
iEnd = oResult.at;
if (sResolvedPath.length === iEnd + 1
|| sResolvedPath.charAt(iEnd + 1) === '/') {
oBinding = oResult.result;
vPart = sResolvedPath.slice(0, iEnd + 1);
sResolvedPath = sResolvedPath.slice(iEnd + 2);
}
} catch (ex) {
if (!(ex instanceof SyntaxError)) {
throw ex;
}
// do nothing, syntax error is logged already
}
}
if (vPart === undefined) {
// No query or unsuccessful query, simply take the next part until '/'
iEnd = sResolvedPath.indexOf("/");
if (iEnd < 0) {
vPart = sResolvedPath;
sResolvedPath = "";
} else {
vPart = sResolvedPath.slice(0, iEnd);
sResolvedPath = sResolvedPath.slice(iEnd + 1);
}
}
if (!oNode) {
if (Log.isLoggable(Log.Level.WARNING, sODataMetaModel)) {
Log.warning("Invalid part: " + vPart,
"path: " + sPath + ", context: "
+ (oContext instanceof Context ? oContext.getPath() : oContext),
sODataMetaModel);
}
break;
}
if (oBinding) {
if (oBaseNode === oContext) {
Log.error(
"A query is not allowed when an object context has been given", sPath,
sODataMetaModel);
return null;
}
if (!Array.isArray(oNode)) {
Log.error(
"Invalid query: '" + sProcessedPath + "' does not point to an array",
sPath, sODataMetaModel);
return null;
}
sCacheKey = sProcessedPath + vPart;
vPart = this.mQueryCache[sCacheKey];
if (vPart === undefined) {
// Set the resolver on the internal JSON model, so that resolving does not use
// this._getObject itself.
this.oResolver = this.oResolver || new Resolver({models: this.oModel});
for (i = 0; i < oNode.length; i += 1) {
this.oResolver.bindObject(sProcessedPath + i);
this.oResolver.bindProperty("any", oBinding);
try {
if (this.oResolver.getAny()) {
this.mQueryCache[sCacheKey] = vPart = i;
break;
}
} finally {
this.oResolver.unbindProperty("any");
this.oResolver.unbindObject();
}
}
}
}
oNode = oNode[vPart];
sProcessedPath = sProcessedPath + vPart + "/";
}
return oNode;
};
/**
* Gets an object containing a shared {@link sap.ui.model.odata.v2.ODataModel} instance, which
* is used to load code lists for currencies and units, and
* <code>bFirstCodeListRequested</code>, which is initially <code>false</code> and is used to
* destroy the shared model at the right time. The <code>ODataMetaModel</code> is able to handle
* two code lists, one for currencies and one for units. As soon as the first code list is
* processed, whether successfully or not, <code>bFirstCodeListRequested</code> is set to
* <code>true</code>. Once a second code list has been processed, the shared model is not needed
* any more and is destroyed. The shared model is also destroyed when this instance of the
* <code>ODataMetaModel</code> gets destroyed.
*
* @returns {object}
* An object containing an OData model and <code>bFirstCodeListRequested</code>
*
* @private
*/
ODataMetaModel.prototype._getOrCreateSharedModelCache = function () {
var oDataModel = this.oDataModel;
if (!this.oSharedModelCache) {
this.oSharedModelCache = {
bFirstCodeListRequested : false,
oModel : new oDataModel.constructor(oDataModel.getCodeListModelParameters())
};
}
return this.oSharedModelCache;
};
/**
* Merges metadata retrieved via <code>this.oDataModel.addAnnotationUrl</code>.
*
* @param {object} oResponse response from addAnnotationUrl.
*
* @private
*/
ODataMetaModel.prototype._mergeMetadata = function (oResponse) {
var oEntityContainer = this.getODataEntityContainer(),
mChildAnnotations = Utils.getChildAnnotations(oResponse.annotations,
oEntityContainer.namespace + "." + oEntityContainer.name, true),
iFirstNewEntitySet = oEntityContainer.entitySet.length,
aSchemas = this.oModel.getObject("/dataServices/schema"),
that = this;
// merge metadata for entity sets/types
oResponse.entitySets.forEach(function (oEntitySet) {
var oEntityType,
oSchema,
sTypeName = oEntitySet.entityType,
// Note: namespaces may contain dots themselves!
sNamespace = sTypeName.slice(0, sTypeName.lastIndexOf("."));
if (!that.getODataEntitySet(oEntitySet.name)) {
oEntityContainer.entitySet.push(JSON.parse(JSON.stringify(oEntitySet)));
if (!that.getODataEntityType(sTypeName)) {
oEntityType = that.oMetadata._getEntityTypeByName(sTypeName);
oSchema = Utils.getSchema(aSchemas, sNamespace);
oSchema.entityType.push(JSON.parse(JSON.stringify(oEntityType)));
// visit all entity types before visiting the entity sets to ensure that V2
// annotations are already lifted up and can be used for calculating entity
// set annotations which are based on V2 annotations on entity properties
Utils.visitParents(oSchema, oResponse.annotations,
"entityType", Utils.visitEntityType,
oSchema.entityType.length - 1);
}
}
});
Utils.visitChildren(oEntityContainer.entitySet, mChildAnnotations, "EntitySet", aSchemas,
/*fnCallback*/null, iFirstNewEntitySet);
};
/**
* Send all currently pending value list requests as a single bundle.
*
* @private
*/
ODataMetaModel.prototype._sendBundledRequest = function () {
var mQName2PendingRequest = this.mQName2PendingRequest, // remember current state
aQualifiedPropertyNames = Object.keys(mQName2PendingRequest),
that = this;
if (!aQualifiedPropertyNames.length) {
return; // nothing to do
}
this.mQName2PendingRequest = {}; // clear pending requests for next bundle
// normalize URL to be browser cache friendly with value list request
aQualifiedPropertyNames = aQualifiedPropertyNames.sort();
aQualifiedPropertyNames.forEach(function (sQualifiedPropertyName, i) {
aQualifiedPropertyNames[i] = encodeURIComponent(sQualifiedPropertyName);
});
this.oDataModel
.addAnnotationUrl("$metadata?sap-value-list=" + aQualifiedPropertyNames.join(","))
.then(
function (oResponse) {
var sQualifiedPropertyName;
that._mergeMetadata(oResponse);
for (sQualifiedPropertyName in mQName2PendingRequest) {
try {
mQName2PendingRequest[sQualifiedPropertyName].resolve(oResponse);
} catch (oError) {
mQName2PendingRequest[sQualifiedPropertyName].reject(oError);
}
}
},
function (oError) {
var sQualifiedPropertyName;
for (sQualifiedPropertyName in mQName2PendingRequest) {
mQName2PendingRequest[sQualifiedPropertyName].reject(oError);
}
}
);
};
ODataMetaModel.prototype.bindContext = function (sPath, oContext, mParameters) {
return new ClientContextBinding(this, sPath, oContext, mParameters);
};
ODataMetaModel.prototype.bindList = function (sPath, oContext, aSorters, aFilters,
mParameters) {
return new ODataMetaListBinding(this, sPath, oContext, aSorters, aFilters, mParameters);
};
ODataMetaModel.prototype.bindProperty = function (sPath, oContext, mParameters) {
return new JSONPropertyBinding(this, sPath, oContext, mParameters);
};
ODataMetaModel.prototype.bindTree = function (sPath, oContext, aFilters, mParameters) {
return new JSONTreeBinding(this, sPath, oContext, aFilters, mParameters);
};
ODataMetaModel.prototype.destroy = function () {
MetaModel.prototype.destroy.apply(this, arguments);
if (this.oSharedModelCache) {
this.oSharedModelCache.oModel.destroy();
delete this.oSharedModelCache;
}
return this.oModel && this.oModel.destroy.apply(this.oModel, arguments);
};
/**
* Requests the customizing based on the code list reference given in the entity container's
* <code>com.sap.vocabularies.CodeList.v1.*</code> annotation for the term specified in the
* <code>sTerm</code> parameter. Once a code list has been requested, the promise is cached.
*
* @param {string} sTerm
* The unqualified name of the term from the <code>com.sap.vocabularies.CodeList.v1</code>
* vocabulary used to annotate the entity container, e.g. "CurrencyCodes" or "UnitsOfMeasure"
* @returns {sap.ui.base.SyncPromise}
* A promise resolving with the customizing, which is a map from the code key to an object
* with the following properties:
* <ul>
* <li>StandardCode: The language-independent standard code (e.g. ISO) for the code as
* referred to via the <code>com.sap.vocabularies.CodeList.v1.StandardCode</code>
* annotation on the code's key, if present
* <li>Text: The language-dependent text for the code as referred to via the
* <code>com.sap.vocabularies.Common.v1.Text</code> annotation on the code's key
* <li>UnitSpecificScale: The decimals for the code as referred to via the
* <code>com.sap.vocabularies.Common.v1.UnitSpecificScale</code> annotation on the code's
* key; entries where this would be <code>null</code> are ignored, and an error is logged
* </ul>
* It resolves with <code>null</code> if no given
* <code>com.sap.vocabularies.CodeList.v1.*</code> annotation is found.
* It is rejected if the code list URL is not "./$metadata", there is not exactly one code
* key, or if the customizing cannot be loaded.
*
* @private
* @see #requestCurrencyCodes
* @see #requestUnitsOfMeasure
*/
ODataMetaModel.prototype.fetchCodeList = function (sTerm) {
var that = this;
return this.oLoadedPromiseSync.then(function () {
var sCacheKey, sCacheKeyWithModel, oCodeListModel, oCodeListModelCache, sCollectionPath,
oMappingPromise, sMetaDataUrl, oPromise, oReadPromise,
sCodeListAnnotation = "com.sap.vocabularies.CodeList.v1." + sTerm,
oCodeListAnnotation = that.getODataEntityContainer()[sCodeListAnnotation];
if (!oCodeListAnnotation
// for backend backward compatibility it may happen that a code list annotation is
// available but the "Url" property has no "String" value -> treat it as if no code
// list is available
|| !oCodeListAnnotation.Url.String) {
return null;
}
if (oCodeListAnnotation.Url.String !== "./$metadata") {
throw new Error(sCodeListAnnotation
+ "/Url/String has to be './$metadata' for the service "
+ that.oDataModel.getCodeListModelParameters().serviceUrl);
}
sCollectionPath = oCodeListAnnotation.CollectionPath.String;
sMetaDataUrl = that.oDataModel.getMetadataUrl();
sCacheKey = sMetaDataUrl + "#" + sCollectionPath;
// check for global cache entry
oPromise = mCodeListUrl2Promise.get(sCacheKey);
if (oPromise) {
return oPromise;
}
// check for an ODataModel related cache entry
sCacheKeyWithModel = sCacheKey + "#" + that.getId();
oPromise = mCodeListUrl2Promise.get(sCacheKeyWithModel);
if (oPromise) {
return oPromise;
}
oCodeListModelCache = that._getOrCreateSharedModelCache();
oCodeListModel = oCodeListModelCache.oModel;
oReadPromise = new SyncPromise(function (fnResolve, fnReject) {
var oUriParams = UriParameters.fromURL(sMetaDataUrl),
sClient = oUriParams.get("sap-client"),
sLanguage = oUriParams.get("sap-language"),
mUrlParameters = {$skip : 0, $top : 5000}; // avoid server-driven paging
if (sClient) {
mUrlParameters["sap-client"] = sClient;
}
if (sLanguage) {
mUrlParameters["sap-language"] = sLanguage;
}
oCodeListModel.read("/" + sCollectionPath, {
error : fnReject,
success : fnResolve,
urlParameters : mUrlParameters
});
});
oMappingPromise = new SyncPromise(function (fnResolve, fnReject) {
try {
fnResolve(that._getPropertyNamesForCodeListCustomizing(sCollectionPath));
} catch (oError) {
// ensure that oPromise gets a value and is cached even if there is an error
// when calling _getPropertyNamesForCodeListCustomizing
fnReject(oError);
}
});
oPromise = SyncPromise.all([oReadPromise, oMappingPromise]).then(function (aResults) {
var aData = aResults[0].results,
mMapping = aResults[1];
mCodeListUrl2Promise.set(sCacheKey, oPromise);
mCodeListUrl2Promise.delete(sCacheKeyWithModel); // not needed any more
return aData.reduce(function (mCode2Customizing, oEntity) {
var sCode = oEntity[mMapping.code],
oCustomizing = {
Text : oEntity[mMapping.text],
UnitSpecificScale : oEntity[mMapping.unitSpecificScale]
};
if (mMapping.standardCode) {
oCustomizing.StandardCode = oEntity[mMapping.standardCode];
}
// ignore customizing where the unit-specific scale is missing; log an error
if (oCustomizing.UnitSpecificScale === null) {
Log.error("Ignoring customizing w/o unit-specific scale for code "
+ sCode + " from " + sCollectionPath,
that.oDataModel.getCodeListModelParameters().serviceUrl,
sODataMetaModel);
} else {
mCode2Customizing[sCode] = oCustomizing;
}
return mCode2Customizing;
}, {});
}).catch(function (oError) {
if (oCodeListModel.bDestroyed) {
// do not cache rejected Promise caused by a destroyed code list model
mCodeListUrl2Promise.delete(sCacheKey);
mCodeListUrl2Promise.delete(sCacheKeyWithModel);
} else {
Log.error("Couldn't load code list: " + sCollectionPath + " for "
+ that.oDataModel.getCodeListModelParameters().serviceUrl,
oError, sODataMetaModel);
}
throw oError;
}).finally(function () {
if (oCodeListModelCache.bFirstCodeListRequested) {
if (!oCodeListModel.bDestroyed) {
oCodeListModel.destroy();
}
delete that.oSharedModelCache;
} else {
oCodeListModelCache.bFirstCodeListRequested = true;
}
});
mCodeListUrl2Promise.set(sCacheKeyWithModel, oPromise);
return oPromise;
});
};
/**
* Returns the OData meta model context corresponding to the given OData model path.
*
* @param {string} [sPath]
* an absolute path pointing to an entity or property, e.g.
* "/ProductSet(1)/ToSupplier/BusinessPartnerID"; this equals the
* <a href="http://www.odata.org/documentation/odata-version-2-0/uri-conventions#ResourcePath">
* resource path</a> component of a URI according to OData V2 URI conventions
* @returns {sap.ui.model.Context|null}
* the context for the corresponding metadata object, i.e. an entity type or its property,
* or <code>null</code> in case no path is given
* @throws {Error} in case no context can be determined
* @public
*/
ODataMetaModel.prototype.getMetaContext = function (sPath) {
var oAssocationEnd,
oEntitySet,
oEntityType,
oFunctionImport,
sMetaPath,
sNavigationPropertyName,
sPart,
aParts,
sQualifiedName; // qualified name of current (entity) type across navigations
/*
* Strips the OData key predicate from a resource path segment.
*
* @param {string} sSegment
* @returns {string}
*/
function stripKeyPredicate(sSegment) {
var iPos = sSegment.indexOf("(");
return iPos >= 0
? sSegment.slice(0, iPos)
: sSegment;
}
if (!sPath) {
return null;
}
aParts = sPath.split("/");
if (aParts[0] !== "") {
throw new Error("Not an absolute path: " + sPath);
}
aParts.shift();
// from entity set to entity type
sPart = stripKeyPredicate(aParts[0]);
oEntitySet = this.getODataEntitySet(sPart);
if (oEntitySet) {
sQualifiedName = oEntitySet.entityType;
} else {
oFunctionImport = this.getODataFunctionImport(sPart);
if (oFunctionImport) {
if (aParts.length === 1) {
sMetaPath = this.getODataFunctionImport(sPart, true);
}
sQualifiedName = oFunctionImport.returnType;
if (sQualifiedName.lastIndexOf("Collection(", 0) === 0) {
sQualifiedName = sQualifiedName.slice(11, -1);
}
} else {
throw new Error("Entity set or function import not found: " + sPart);
}
}
aParts.shift();
// follow (navigation) properties
while (aParts.length) {
oEntityType = this.getODataEntityType(sQualifiedName);
if (oEntityType) {
sNavigationPropertyName = stripKeyPredicate(aParts[0]);
oAssocationEnd = this.getODataAssociationEnd(oEntityType, sNavigationPropertyName);
} else { // function import's return type may be a complex type
oEntityType = this.getODataComplexType(sQualifiedName);
}
if (oAssocationEnd) {
// navigation property (Note: can appear in entity types, but not complex types)
sQualifiedName = oAssocationEnd.type;
if (oAssocationEnd.multiplicity === "1" && sNavigationPropertyName !== aParts[0]) {
// key predicate not allowed here
throw new Error("Multiplicity is 1: " + aParts[0]);
}
aParts.shift();
} else {
// structural property, incl. complex types
sMetaPath = this.getODataProperty(oEntityType, aParts, true);
if (aParts.length) {
throw new Error("Property not found: " + aParts.join("/"));
}
break;
}
}
sMetaPath = sMetaPath || this.getODataEntityType(sQualifiedName, true);
return this.createBindingContext(sMetaPath);
};
/**
* Returns the OData association end corresponding to the given entity type's navigation
* property of given name.
*
* @param {object} oEntityType
* an entity type as returned by {@link #getODataEntityType getODataEntityType}
* @param {string} sName
* the name of a navigation property within this entity type
* @returns {object|null}
* the OData association end or <code>null</code> if no such association end is found
* @public
*/
ODataMetaModel.prototype.getODataAssociationEnd = function (oEntityType, sName) {
var oNavigationProperty = oEntityType
? Utils.findObject(oEntityType.navigationProperty, sName)
: null,
oAssociation = oNavigationProperty
? Utils.getObject(this.oModel, "association", oNavigationProperty.relationship)
: null,
oAssociationEnd = oAssociation
? Utils.findObject(oAssociation.end, oNavigationProperty.toRole, "role")
: null;
return oAssociationEnd;
};
/**
* Returns the OData association <em>set</em> end corresponding to the given entity type's
* navigation property of given name.
*
* @param {object} oEntityType
* an entity type as returned by {@link #getODataEntityType getODataEntityType}
* @param {string} sName
* the name of a navigation property within this entity type
* @returns {object|null}
* the OData association set end or <code>null</code> if no such association set end is found
* @public
*/
ODataMetaModel.prototype.getODataAssociationSetEnd = function (oEntityType, sName) {
var oAssociationSet,
oAssociationSetEnd = null,
oEntityContainer = this.getODataEntityContainer(),
oNavigationProperty = oEntityType
? Utils.findObject(oEntityType.navigationProperty, sName)
: null;
if (oEntityContainer && oNavigationProperty) {
oAssociationSet = Utils.findObject(oEntityContainer.associationSet,
oNavigationProperty.relationship, "association");
oAssociationSetEnd = oAssociationSet
? Utils.findObject(oAssociationSet.end, oNavigationProperty.toRole, "role")
: null;
}
return oAssociationSetEnd;
};
/**
* Returns the OData complex type with the given qualified name, either as a path or as an
* object, as indicated.
*
* @param {string} sQualifiedName
* a qualified name, e.g. "ACME.Address"
* @param {boolean} [bAsPath=false]
* determines whether the complex type is returned as a path or as an object
* @returns {object|string|undefined|null}
* (the path to) the complex type with the given qualified name; <code>undefined</code> (for
* a path) or <code>null</code> (for an object) if no such type is found
* @public
*/
ODataMetaModel.prototype.getODataComplexType = function (sQualifiedName, bAsPath) {
return Utils.getObject(this.oModel, "complexType", sQualifiedName, bAsPath);
};
/**
* Returns the OData default entity container. If there is only a single schema with a single
* entity container, the entity container does not need to be marked as default explicitly.
*
* @param {boolean} [bAsPath=false]
* determines whether the entity container is returned as a path or as an object
* @returns {object|string|undefined|null}
* (the path to) the default entity container; <code>undefined</code> (for a path) or
* <code>null</code> (for an object) if no such container is found
* @public
*/
ODataMetaModel.prototype.getODataEntityContainer = function (bAsPath) {
var vResult = bAsPath ? undefined : null,
aSchemas = this.oModel.getObject("/dataServices/schema");
if (aSchemas) {
aSchemas.forEach(function (oSchema, i) {
var j = Utils.findIndex(oSchema.entityContainer, "true",
"isDefaultEntityContainer");
if (j >= 0) {
vResult = bAsPath
? "/dataServices/schema/" + i + "/entityContainer/" + j
: oSchema.entityContainer[j];
}
});
if (!vResult && aSchemas.length === 1 && aSchemas[0].entityContainer
&& aSchemas[0].entityContainer.length === 1) {
vResult = bAsPath
? "/dataServices/schema/0/entityContainer/0"
: aSchemas[0].entityContainer[0];
}
}
return vResult;
};
/**
* Returns the OData entity set with the given simple name from the default entity container.
*
* @param {string} sName
* a simple name, e.g. "ProductSet"
* @param {boolean} [bAsPath=false]
* determines whether the entity set is returned as a path or as an object
* @returns {object|string|undefined|null}
* (the path to) the entity set with the given simple name; <code>undefined</code> (for a
* path) or <code>null</code> (for an object) if no such set is found
* @public
*/
ODataMetaModel.prototype.getODataEntitySet = function (sName, bAsPath) {
return Utils.getFromContainer(this.getODataEntityContainer(), "entitySet", sName, bAsPath);
};
/**
* Returns the OData entity type with the given qualified name, either as a path or as an
* object, as indicated.
*
* @param {string} sQualifiedName
* a qualified name, e.g. "ACME.Product"
* @param {boolean} [bAsPath=false]
* determines whether the entity type is returned as a path or as an object
* @returns {object|string|undefined|null}
* (the path to) the entity type with the given qualified name; <code>undefined</code> (for a
* path) or <code>null</code> (for an object) if no such type is found
* @public
*/
ODataMetaModel.prototype.getODataEntityType = function (sQualifiedName, bAsPath) {
return Utils.getObject(this.oModel, "entityType", sQualifiedName, bAsPath);
};
/**
* Returns the OData function import with the given simple or qualified name from the default
* entity container or the respective entity container specified in the qualified name.
*
* @param {string} sName
* a simple or qualified name, e.g. "Save" or "MyService.Entities/Save"
* @param {boolean} [bAsPath=false]
* determines whether the function import is returned as a path or as an object
* @returns {object|string|undefined|null}
* (the path to) the function import with the given simple name; <code>undefined</code> (for
* a path) or <code>null</code> (for an object) if no such function import is found
* @public
* @since 1.29.0
*/
ODataMetaModel.prototype.getODataFunctionImport = function (sName, bAsPath) {
var aParts = sName && sName.indexOf('/') >= 0 ? sName.split('/') : undefined,
oEntityContainer = aParts ?
Utils.getObject(this.oModel, "entityContainer", aParts[0]) :
this.getODataEntityContainer();
return Utils.getFromContainer(oEntityContainer, "functionImport",
aParts ? aParts[1] : sName, bAsPath);
};
/**
* Returns the given OData type's property (not navigation property!) of given name.
*
* If an array is given instead of a single name, it is consumed (via
* <code>Array.prototype.shift</code>) piece by piece. Each element is interpreted as a
* property name of the current type, and the current type is replaced by that property's type.
* This is repeated until an element is encountered which cannot be resolved as a property name
* of the current type anymore; in this case, the last property found is returned and
* <code>vName</code> contains only the remaining names, with <code>vName[0]</code> being the
* one which was not found.
*
* Examples:
* <ul>
* <li> Get address property of business partner:
* <pre>
* var oEntityType = oMetaModel.getODataEntityType("GWSAMPLE_BASIC.BusinessPartner"),
* oAddressProperty = oMetaModel.getODataProperty(oEntityType, "Address");
* oAddressProperty.name === "Address";
* oAddressProperty.type === "GWSAMPLE_BASIC.CT_Address";
* </pre>
* </li>
* <li> Get street property of address type:
* <pre>
* var oComplexType = oMetaModel.getODataComplexType("GWSAMPLE_BASIC.CT_Address"),
* oStreetProperty = oMetaModel.getODataProperty(oComplexType, "Street");
* oStreetProperty.name === "Street";
* oStreetProperty.type === "Edm.String";
* </pre>
* </li>
* <li> Get address' street property directly from business partner:
* <pre>
* var aParts = ["Address", "Street"];
* oMetaModel.getODataProperty(oEntityType, aParts) === oStreetProperty;
* aParts.length === 0;
* </pre>
* </li>
* <li> Trying to get address' foo property directly from business partner:
* <pre>
* aParts = ["Address", "foo"];
* oMetaModel.getODataProperty(oEntityType, aParts) === oAddressProperty;
* aParts.length === 1;
* aParts[0] === "foo";
* </pre>
* </li>
* </ul>
*
* @param {object} oType
* a complex type as returned by {@link #getODataComplexType getODataComplexType}, or
* an entity type as returned by {@link #getODataEntityType getODataEntityType}
* @param {string|string[]} vName
* the name of a property within this type (e.g. "Address"), or an array of such names (e.g.
* <code>["Address", "Street"]</code>) in order to drill-down into complex types;
* <b>BEWARE</b> that this array is modified by removing each part which is understood!
* @param {boolean} [bAsPath=false]
* determines whether the property is returned as a path or as an object
* @returns {object|string|undefined|null}
* (the path to) the last OData property found; <code>undefined</code> (for a path) or
* <code>null</code> (for an object) if no property was found at all
* @public
*/
ODataMetaModel.prototype.getODataProperty = function (oType, vName, bAsPath) {
var i,
aParts = Array.isArray(vName) ? vName : [vName],
oProperty = null,
sPropertyPath;
while (oType && aParts.length) {
i = Utils.findIndex(oType.property, aParts[0]);
if (i < 0) {
break;
}
aParts.shift();
oProperty = oType.property[i];
sPropertyPath = oType.$path + "/property/" + i;
if (aParts.length) {
// go to complex type in order to allow drill-down
oType = this.getODataComplexType(oProperty.type);
}
}
return bAsPath ? sPropertyPath : oProperty;
};
/**
* Returns a <code>Promise</code> which is resolved with a map representing the
* <code>com.sap.vocabularies.Common.v1.ValueList</code> annotations of the given property or
* rejected with an error.
* The key in the map provided on successful resolution is the qualifier of the annotation or
* the empty string if no qualifier is defined. The value in the map is the JSON object for
* the annotation. The map is empty if the property has no
* <code>com.sap.vocabularies.Common.v1.ValueList</code> annotations.
*
* @param {sap.ui.model.Context} oPropertyContext
* a model context for a structural property of an entity type or a complex type, as
* returned by {@link #getMetaContext getMetaContext}
* @returns {Promise}
* a Promise that gets resolved as soon as the value lists as well as the required model
* elements have been loaded
* @since 1.29.1
* @public
*/
ODataMetaModel.prototype.getODataValueLists = function (oPropertyContext) {
var bCachePromise = false, // cache only promises which trigger a request
aMatches,
sPropertyPath = oPropertyContext.getPath(),
oPromise = this.mContext2Promise[sPropertyPath],
that = this;
if (oPromise) {
return oPromise;
}
aMatches = rPropertyPath.exec(sPropertyPath);
if (!aMatches) {
throw new Error("Unsupported property context with path " + sPropertyPath);
}
oPromise = new Promise(function (fnResolve, fnReject) {
var oProperty = oPropertyContext.getObject(),
sQualifiedTypeName,
mValueLists = Utils.getValueLists(oProperty);
if (!("" in mValueLists) && oProperty["sap:value-list"]) {
// property with value list which is not yet (fully) loaded
bCachePromise = true;
sQualifiedTypeName = that.oModel.getObject(aMatches[2]).namespace
+ "." + that.oModel.getObject(aMatches[1]).name;
that.mQName2PendingRequest[sQualifiedTypeName + "/" + oProperty.name] = {
resolve : function (oResponse) {
// enhance property by annotations from response to get value lists
extend(oProperty,
(oResponse.annotations.propertyAnnotations[sQualifiedTypeName] || {})
[oProperty.name]
);
mValueLists = Utils.getValueLists(oProperty);
if (isEmptyObject(mValueLists)) {
fnReject(new Error("No value lists returned for " + sPropertyPath));
} else {
delete that.mContext2Promise[sPropertyPath];
fnResolve(mValueLists);
}
},
reject : fnReject
};
// send bundled value list request once after multiple synchronous API calls
setTimeout(that._sendBundledRequest.bind(that), 0);
} else {
fnResolve(mValueLists);
}
});
if (bCachePromise) {
this.mContext2Promise[sPropertyPath] = oPromise;
}
return oPromise;
};
ODataMetaModel.prototype.getProperty = function () {
return this._getObject.apply(this, arguments);
};
ODataMetaModel.prototype.isList = function () {
return this.oModel.isList.apply(this.oModel, arguments);
};
/**
* Returns a promise which is fulfilled once the meta model data is loaded and can be used.
*
* @public
* @returns {Promise} a Promise
*/
ODataMetaModel.prototype.loaded = function () {
return this.oLoadedPromise;
};
/**
* Refresh not supported by OData meta model!
*
* @throws {Error}
* @public
*/
ODataMetaModel.prototype.refresh = function () {
throw new Error("Unsupported operation: ODataMetaModel#refresh");
};
/**
* Requests the currency customizing based on the code list reference given in the entity
* container's <code>com.sap.vocabularies.CodeList.v1.CurrencyCodes</code> annotation. The
* corresponding HTTP request uses the HTTP headers obtained via
* {@link sap.ui.model.odata.v2.ODataModel#getHeaders} from this meta model's data model.
*
* @returns {Promise}
* A promise resolving with the currency customizing, which is a map from the currency key to
* an object with the following properties:
* <ul>
* <li><code>StandardCode</code>: The language-independent standard code (e.g. ISO) for the
* currency as referred to via the
* <code>com.sap.vocabularies.CodeList.v1.StandardCode</code> annotation on the currency's
* key, if present
* <li><code>Text</code>: The language-dependent text for the currency as referred to via
* the <code>com.sap.vocabularies.Common.v1.Text</code> annotation on the currency's key
* <li><code>UnitSpecificScale</code>: The decimals for the currency as referred to via the
* <code>com.sap.vocabularies.Common.v1.UnitSpecificScale</code> annotation on the
* currency's key; entries where this would be <code>null</code> are ignored, and an error
* is logged
* </ul>
* It resolves with <code>null</code> if no
* <code>com.sap.vocabularies.CodeList.v1.CurrencyCodes</code> annotation is found.
* It is rejected if the code list URL is not "./$metadata", there is not exactly one code
* key, or if the customizing cannot be loaded.
*
* @public
* @see {@link #requestUnitsOfMeasure}
* @since 1.88.0
*/
ODataMetaModel.prototype.requestCurrencyCodes = function () {
return Promise.resolve(this.fetchCodeList("CurrencyCodes")).then(function (mCodeList) {
return mCodeList ? _Helper.merge({}, mCodeList) : mCodeList;
});
};
/**
* Requests the unit customizing based on the code list reference given in the entity
* container's <code>com.sap.vocabularies.CodeList.v1.UnitOfMeasure</code> annotation. The
* corresponding HTTP request uses the HTTP headers obtained via
* {@link sap.ui.model.odata.v2.ODataModel#getHeaders} from this meta model's data model.
*
* @returns {Promise}
* A promise resolving with the unit customizing, which is a map from the unit key to an
* object with the following properties:
* <ul>
* <li><code>StandardCode</code>: The language-independent standard code (e.g. ISO) for the
* unit as referred to via the <code>com.sap.vocabularies.CodeList.v1.StandardCode</code>
* annotation on the unit's key, if present
* <li><code>Text</code>: The language-dependent text for the unit as referred to via the
* <code>com.sap.vocabularies.Common.v1.Text</code> annotation on the unit's key
* <li><code>UnitSpecificScale</code>: The decimals for the unit as referred to via the
* <code>com.sap.vocabularies.Common.v1.UnitSpecificScale</code> annotation on the unit's
* key; entries where this would be <code>null</code> are ignored, and an error is logged
* </ul>
* It resolves with <code>null</code> if no
* <code>com.sap.vocabularies.CodeList.v1.UnitOfMeasure</code> annotation is found.
* It is rejected if the code list URL is not "./$metadata", there is not exactly one code
* key, or if the customizing cannot be loaded.
*
* @public
* @see {@link #requestCurrencyCodes}
* @since 1.88.0
*/
ODataMetaModel.prototype.requestUnitsOfMeasure = function () {
return Promise.resolve(this.fetchCodeList("UnitsOfMeasure")).then(function (mCodeList) {
return mCodeList ? _Helper.merge({}, mCodeList) : mCodeList;
});
};
/**
* Legacy syntax not supported by OData meta model!
*
* @param {boolean} bLegacySyntax
* must not be true!
* @throws {Error} if <code>bLegacySyntax</code> is true
* @public
*/
ODataMetaModel.prototype.setLegacySyntax = function (bLegacySyntax) {
if (bLegacySyntax) {
throw new Error("Legacy syntax not supported by ODataMetaModel");
}
};
/**
* Changes not supported by OData meta model!
*
* @throws {Error}
* @private
*/
ODataMetaModel.prototype.setProperty = function () {
// Note: this method is called by JSONPropertyBinding#setValue
throw new Error("Unsupported operation: ODataMetaModel#setProperty");
};
/**
* Gets the property names for the code list customizing for the given code list collection
* path.
*
* In some cases it might be necessary to overwrite code list annotations contained in the
* service metadata document. So local annotations need to be considered when loading code
* lists. As code lists have to be provided by the same service as the current data model is
* using, the metadata of the data model can be used to determine the property names
* for the code list customizing. In that case also annotations added via
* {@link sap.ui.model.odata.v2.ODataModel#addAnnotationUrl} or
* {@link sap.ui.model.odata.v2.ODataModel#addAnnotationXML} are considered.
*
* @param {string} sCollectionPath
* The collection path specified in the corresponding
* com.sap.vocabularies.CodeList.v1.* annotation e.g. "SAP__Currencies"
* @returns {object}
* The returned object has the properties "code", "text", "unitSpecificScale" and
* optionally "standardCode", with the values for the corresponding property names of the
* entity representing a code list entry
* @throws {Error}
* If there is more than one alternative or more than one key per alternative
*
* @private
*/
ODataMetaModel.prototype._getPropertyNamesForCodeListCustomizing = function (sCollectionPath) {
var sPathToCollectionMetadata = "/" + sCollectionPath + "/##",
oTypeMetadata = this.oDataModel.getObject(sPathToCollectionMetadata),
aAlternateKeys = oTypeMetadata["Org.OData.Core.V1.AlternateKeys"],
sKeyPath = ODataMetaModel._getKeyPath(oTypeMetadata, sPathToCollectionMetadata),
oKeyMetadata = this.oDataModel.getObject("/" + sCollectionPath + "/" + sKeyPath
+ "/##");
if (aAlternateKeys) {
if (aAlternateKeys.length !== 1) {
throw new Error("Single alterna