@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,018 lines (942 loc) • 36.7 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/*eslint-disable max-len */
/**
* OData-based DataBinding Utility Class
*
* @namespace
* @name sap.ui.model.odata
* @public
*/
// Provides class sap.ui.model.odata.ODataUtils
sap.ui.define([
"sap/base/assert",
"sap/base/Log",
"sap/base/i18n/date/CalendarType",
"sap/base/security/encodeURL",
"sap/base/util/each",
"sap/ui/core/format/DateFormat",
"sap/ui/model/_Helper",
"sap/ui/model/FilterProcessor",
"sap/ui/model/Sorter"
], function(assert, Log, CalendarType, encodeURL, each, DateFormat, _Helper, FilterProcessor, Sorter) {
"use strict";
let oDateTimeFormat, oDateTimeFormatMs, oDateTimeOffsetFormat, oDateTimeOffsetFormatMs, oTimeFormat;
const sClassName = "sap.ui.model.odata.ODataUtils";
const rDecimal = /^([-+]?)0*(\d+)(\.\d+|)$/;
// URL might be encoded, "(" becomes %28
const rSegmentAfterCatalogService = /\/(Annotations|ServiceNames|ServiceCollection)(\(|%28)/;
const rTrailingDecimal = /\.$/;
const rTrailingSingleQuote = /'$/;
const rTrailingZeroes = /0+$/;
function setDateTimeFormatter () {
// Lazy creation of format objects
if (!oDateTimeFormat) {
oDateTimeFormat = DateFormat.getDateInstance({
pattern: "'datetime'''yyyy-MM-dd'T'HH:mm:ss''",
calendarType: CalendarType.Gregorian
});
oDateTimeFormatMs = DateFormat.getDateInstance({
pattern: "'datetime'''yyyy-MM-dd'T'HH:mm:ss.SSS''",
calendarType: CalendarType.Gregorian
});
oDateTimeOffsetFormat = DateFormat.getDateInstance({
pattern: "'datetimeoffset'''yyyy-MM-dd'T'HH:mm:ss'Z'''",
calendarType: CalendarType.Gregorian
});
oDateTimeOffsetFormatMs = DateFormat.getDateInstance({
pattern: "'datetimeoffset'''yyyy-MM-dd'T'HH:mm:ss.SSS'Z'''",
calendarType: CalendarType.Gregorian
});
oTimeFormat = DateFormat.getTimeInstance({
pattern: "'time''PT'HH'H'mm'M'ss'S'''",
calendarType: CalendarType.Gregorian
});
}
}
// Static class
/**
* @alias sap.ui.model.odata.ODataUtils
* @namespace
* @public
*/
var ODataUtils = function() {};
/**
* Create URL parameters for sorting
* @param {array} aSorters an array of sap.ui.model.Sorter
* @return {string} the URL encoded sorter parameters
* @private
*/
ODataUtils.createSortParams = function(aSorters) {
var sSortParam;
if (!aSorters || aSorters.length == 0) {
return undefined;
}
sSortParam = "$orderby=";
for (var i = 0; i < aSorters.length; i++) {
var oSorter = aSorters[i];
if (oSorter instanceof Sorter) {
sSortParam += oSorter.sPath;
sSortParam += oSorter.bDescending ? "%20desc" : "%20asc";
sSortParam += ",";
} else {
Log.error("Trying to use " + oSorter + " as a Sorter, but it is a " + typeof oSorter);
}
}
//remove trailing comma
sSortParam = sSortParam.slice(0, -1);
return sSortParam;
};
function convertLegacyFilter(oFilter) {
// check if sap.ui.model.odata.Filter is used. If yes, convert it to sap.ui.model.Filter
if (oFilter && typeof oFilter.convert === "function") {
oFilter = oFilter.convert();
}
return oFilter;
}
/**
* Creates the URL parameter string for filtering. The parameter string is prepended with the "$filter=" system
* query option to form a valid URL part for an OData request. In case an array of filters is passed, they are
* grouped in a way that filters on the same path are OR-ed and filters on different paths are AND-ed with each
* other.
*
* @param {sap.ui.model.Filter|sap.ui.model.Filter[]} vFilter The filter or filter array
* @param {sap.ui.model.odata.ODataMetadata} oMetadata The model metadata
* @param {sap.ui.model.odata.ODataMetaModel.EntityType} oEntityType The entity type
* @return {string} The URL encoded <code>$filter</code> system query option
*
* @private
*/
ODataUtils.createFilterParams = function(vFilter, oMetadata, oEntityType) {
var oFilter;
if (Array.isArray(vFilter)) {
vFilter = vFilter.map(convertLegacyFilter);
oFilter = FilterProcessor.groupFilters(vFilter);
} else {
oFilter = convertLegacyFilter(vFilter);
}
if (!oFilter) {
return undefined;
}
return "$filter=" + this._createFilterParams(oFilter, oMetadata, oEntityType);
};
/**
* Creates a string of logically (or/and) linked filter options, which can be used as a value for the
* <code>$filter</code> system query option.
*
* @param {sap.ui.model.Filter|sap.ui.model.Filter[]} vFilter The filter or filter array
* @param {sap.ui.model.odata.ODataMetadata} oMetadata The model metadata
* @param {sap.ui.model.odata.ODataMetaModel.EntityType} oEntityType The entity type
* @return {string} The URL encoded value for <code>$filter</code> system query option
*
* @private
*/
ODataUtils._createFilterParams = function(vFilter, oMetadata, oEntityType) {
const oFilter = Array.isArray(vFilter) ? FilterProcessor.groupFilters(vFilter) : vFilter;
if (!oFilter) {
return undefined;
}
return ODataUtils._processSingleFilter(oFilter, oMetadata, oEntityType, true);
};
/**
* Gets the filter string for a given filter object.
*
* @param {sap.ui.model.Filter} oFilter The filter object to be processed
* @param {sap.ui.model.odata.ODataMetadata} oMetadata The model metadata
* @param {sap.ui.model.odata.ODataMetaModel.EntityType} oEntityType The entity type
* @param {boolean} [bOmitBrackets] Whether to omit brackets around the resulting filter string
* @returns {string} The URL encoded string representation of the given filter object
*
* @private
*/
ODataUtils._processSingleFilter = function (oFilter, oMetadata, oEntityType, bOmitBrackets) {
oFilter = convertLegacyFilter(oFilter);
if (oFilter.aFilters) {
return ODataUtils._processMultiFilter(oFilter, oMetadata, oEntityType, bOmitBrackets);
}
return ODataUtils._createFilterSegment(oFilter, oMetadata, oEntityType);
};
/**
* Gets the filter string for a given multi-filter object.
*
* @param {sap.ui.model.Filter} oFilter The multi-filter object to be processed
* @param {sap.ui.model.odata.ODataMetadata} oMetadata The model metadata
* @param {sap.ui.model.odata.ODataMetaModel.EntityType} oEntityType The entity type
* @param {boolean} [bOmitBrackets] Whether to omit brackets around the resulting filter string
* @returns {string} The URL encoded string representation of the given multi-filter object
*
* @private
*/
ODataUtils._processMultiFilter = function (oFilter, oMetadata, oEntityType, bOmitBrackets) {
const aFilters = oFilter.aFilters;
const bAnd = !!oFilter.bAnd;
if (aFilters.length === 0) {
return bAnd ? "true" : "false";
}
if (aFilters.length === 1) {
if (aFilters[0]._bMultiFilter) {
return ODataUtils._processSingleFilter(aFilters[0], oMetadata, oEntityType);
}
return ODataUtils._processSingleFilter(aFilters[0], oMetadata, oEntityType, true);
}
return (!bOmitBrackets ? "(" : "")
+ aFilters.map((oFilter) => {
return ODataUtils._processSingleFilter(oFilter, oMetadata, oEntityType);
}).join(bAnd ? "%20and%20" : "%20or%20")
+ (!bOmitBrackets ? ")" : "");
};
/**
* Converts a string or object-map with URL parameters into an array.
* If <code>vParams</code> is an object map, it will be also encoded properly.
*
* @param {string|object|array} vParams URL parameters
* @returns {string[]} Encoded URL parameters
*
* @private
*/
ODataUtils._createUrlParamsArray = function(vParams) {
var aUrlParams, sType = typeof vParams, sParams;
if (Array.isArray(vParams)) {
return vParams;
}
aUrlParams = [];
if (sType === "string" || vParams instanceof String) {
if (vParams) {
aUrlParams.push(vParams);
}
} else if (sType === "object") {
sParams = this._encodeURLParameters(vParams);
if (sParams) {
aUrlParams.push(sParams);
}
}
return aUrlParams;
};
/**
* Encode a map of parameters into a combined URL parameter string
*
* @param {map} mParams The map of parameters to encode
* @returns {string} sUrlParams The URL encoded parameters
* @private
*/
ODataUtils._encodeURLParameters = function(mParams) {
if (!mParams) {
return "";
}
var aUrlParams = [];
each(mParams, function (sName, oValue) {
if (typeof oValue === "string" || oValue instanceof String) {
oValue = encodeURIComponent(oValue);
}
sName = sName.startsWith('$') ? sName : encodeURIComponent(sName);
aUrlParams.push(sName + "=" + oValue);
});
return aUrlParams.join("&");
};
/**
* Adds an origin to the given service URL.
* If an origin is already present, it will only be replaced if the parameters object contains the flag "force: true".
* In case the URL already contains URL parameters, these will be kept.
* As a parameter, a sole alias is sufficient. The parameters vParameters.system and vParameters.client however have to be given in pairs.
* In case all three origin specifying parameters are given (system/client/alias), the alias has precedence.
*
* Examples:
* setOrigin("/backend/service/url/", "DEMO_123");
* - result: /backend/service/url;o=DEMO_123/
*
* setOrigin("/backend/service/url;o=OTHERSYS8?myUrlParam=true&x=4", {alias: "DEMO_123", force: true});
* - result /backend/service/url;o=DEMO_123?myUrlParam=true&x=4
*
* setOrigin("/backend/service;o=NOT_TOUCHED/url;v=2;o=OTHERSYS8;srv=XVC", {alias: "DEMO_123", force: true});
* - result /backend/service;o=NOT_TOUCHED/url;v=2;o=DEMO_123;srv=XVC
*
* setOrigin("/backend/service/url/", {system: "DEMO", client: 134});
* - result /backend/service/url;o=sid(DEMO.134)/
*
* @param {string} sServiceURL the URL which will be enriched with an origin
* @param {object|string} vParameters if string then it is asumed its the system alias, else if the argument is an object then additional Parameters can be given
* @param {string} vParameters.alias the system alias which will be used as the origin
* @param {string} vParameters.system the system id which will be used as the origin
* @param {string} vParameters.client the system's client
* @param {boolean} vParameters.force setting this flag to <code>true</code> overrides the already existing origin
*
* @public
* @since 1.30.7
* @returns {string} the service URL with the added origin.
*/
ODataUtils.setOrigin = function (sServiceURL, vParameters) {
var sOrigin, sSystem, sClient;
// if multi origin is set, do nothing
if (!sServiceURL || !vParameters || sServiceURL.indexOf(";mo") > 0) {
return sServiceURL;
}
// accept string as second argument -> only alias given
if (typeof vParameters == "string") {
sOrigin = vParameters;
} else {
// vParameters is an object
sOrigin = vParameters.alias;
if (!sOrigin) {
sSystem = vParameters.system;
sClient = vParameters.client;
// sanity check
if (!sSystem || !sClient) {
Log.warning("ODataUtils.setOrigin: No Client or System ID given for Origin");
return sServiceURL;
}
sOrigin = "sid(" + sSystem + "." + sClient + ")";
}
}
// determine the service base url and the url parameters
var aUrlParts = sServiceURL.split("?");
var sBaseURL = aUrlParts[0];
var sURLParams = aUrlParts[1] ? "?" + aUrlParts[1] : "";
//trim trailing "/" from url if present
var sTrailingSlash = "";
if (sBaseURL[sBaseURL.length - 1] === "/") {
sBaseURL = sBaseURL.substring(0, sBaseURL.length - 1);
sTrailingSlash = "/"; // append the trailing slash later if necessary
}
// origin already included
// regex will only match ";o=" occurrences which do not end in a slash "/" at the end of the string.
// The last ";o=" occurrence at the end of the baseURL is the only origin that can match.
var rSegmentCheck = /(\/[^\/]+)$/g;
var rOriginCheck = /(;o=[^\/;]+)/g;
var sLastSegment = sBaseURL.match(rSegmentCheck)[0];
var aLastOrigin = sLastSegment.match(rOriginCheck);
var sFoundOrigin = aLastOrigin ? aLastOrigin[0] : null;
if (sFoundOrigin) {
// enforce new origin
if (vParameters.force) {
// same regex as above
var sChangedLastSegment = sLastSegment.replace(sFoundOrigin, ";o=" + sOrigin);
sBaseURL = sBaseURL.replace(sLastSegment, sChangedLastSegment);
return sBaseURL + sTrailingSlash + sURLParams;
}
//return the URL as it was
return sServiceURL;
}
// new service url with origin
sBaseURL = sBaseURL + ";o=" + sOrigin + sTrailingSlash;
return sBaseURL + sURLParams;
};
/**
* Adds an origin to annotation urls.
* Checks if the annotation is based on a catalog service or it's a generic annotation url, which might be adapted based on the service url.
* The actual url modification is done with the setOrigin function.
*
* @param {string} sAnnotationURL the URL which will be enriched with an origin
* @param {object|string} vParameters explanation see setOrigin function
* @param {string} vParameters.preOriginBaseUri Legacy: Service url base path before adding an origin
* @param {string} vParameters.postOriginBaseUri Legacy: Service url base path after adding an origin
* @private
* @since 1.44.0
* @returns {string} the annotation service URL with the added origin.
*/
ODataUtils.setAnnotationOrigin = function(sAnnotationURL, vParameters){
var sFinalAnnotationURL;
var iSegmentAfterCatalogServiceIndex = sAnnotationURL.search(rSegmentAfterCatalogService);
var iHanaXsSegmentIndex = vParameters && vParameters.preOriginBaseUri ? vParameters.preOriginBaseUri.indexOf(".xsodata") : -1;
if (iSegmentAfterCatalogServiceIndex >= 0) {
if (sAnnotationURL.indexOf("/$value", iSegmentAfterCatalogServiceIndex) === -1) { // $value missing
Log.warning("ODataUtils.setAnnotationOrigin: Annotation url is missing $value segment.");
sFinalAnnotationURL = sAnnotationURL;
} else {
// if the annotation URL is an SAP specific annotation url, we add the origin path segment...
var sAnnotationUrlBase = sAnnotationURL.substring(0, iSegmentAfterCatalogServiceIndex);
var sAnnotationUrlRest = sAnnotationURL.substring(iSegmentAfterCatalogServiceIndex, sAnnotationURL.length);
var sAnnotationWithOrigin = ODataUtils.setOrigin(sAnnotationUrlBase, vParameters);
sFinalAnnotationURL = sAnnotationWithOrigin + sAnnotationUrlRest;
}
} else if (iHanaXsSegmentIndex >= 0) {
// Hana XS case: the Hana XS engine can provide static Annotation files for its
// services. The services can be identified by their URL segment ".xsodata"; if such a
// service uses the origin feature the Annotation URLs need also adaption.
sFinalAnnotationURL = ODataUtils.setOrigin(sAnnotationURL, vParameters);
} else {
// Legacy Code for compatibility reasons:
// ... if not, we check if the annotation url is on the same service-url base-path
sFinalAnnotationURL = sAnnotationURL.replace(vParameters.preOriginBaseUri, vParameters.postOriginBaseUri);
}
return sFinalAnnotationURL;
};
/**
* Convert multi-filter to filter string.
*
* @param {object} oMultiFilter A multi-filter
* @param {sap.ui.model.odata.ODataMetadata} oMetadata The model metadata
* @param {sap.ui.model.odata.ODataMetaModel.EntityType} oEntityType The entity type
* @returns {string} The URL encoded string representation of the given multi-filter object
*
* @private
*/
ODataUtils._resolveMultiFilter = function(oMultiFilter, oMetadata, oEntityType){
const aFilters = oMultiFilter.aFilters;
if (aFilters) {
return "("
+ aFilters.map((oFilter) => {
let sFilterParam = "";
if (oFilter._bMultiFilter) {
sFilterParam = ODataUtils._resolveMultiFilter(oFilter, oMetadata, oEntityType);
} else if (oFilter.sPath) {
sFilterParam = ODataUtils._createFilterSegment(oFilter, oMetadata, oEntityType);
}
return sFilterParam;
}).join(oMultiFilter.bAnd ? "%20and%20" : "%20or%20")
+ ")";
}
return "";
};
/**
* Create a single filter segment for the given filter.
*
* @param {sap.ui.model.Filter} oFilter The filter object to be processed
* @param {sap.ui.model.odata.ODataMetadata} oMetadata The model metadata
* @param {sap.ui.model.odata.ODataMetaModel.EntityType} oEntityType The entity type object
* @returns {string} The encoded string representation of the given filter
* @private
*/
ODataUtils._createFilterSegment = function(oFilter, oMetadata, oEntityType) {
let {sPath, oValue1, oValue2} = oFilter;
const {sOperator, bCaseSensitive = true, sFractionalSeconds1, sFractionalSeconds2} = oFilter;
let sType;
if (oEntityType) {
const oPropertyMetadata = oMetadata._getPropertyMetadata(oEntityType, sPath);
if (oPropertyMetadata) {
sType = oPropertyMetadata.type;
if (sType) {
oValue1 = ODataUtils._formatValue(oValue1, sType, bCaseSensitive, sFractionalSeconds1);
oValue2 = oValue2 === null || oValue2 === undefined
? null
: ODataUtils._formatValue(oValue2, sType, bCaseSensitive, sFractionalSeconds2);
} else {
Log.error("Type for property '" + sPath + "' of EntityType '" + oEntityType.name
+ "' not found!", undefined, sClassName);
}
} else {
Log.error("Property type for property '" + sPath + "' of EntityType '" + oEntityType.name
+ "' not found!", undefined, sClassName);
}
}
if (oValue1) {
oValue1 = _Helper.encodeURL(String(oValue1));
}
if (oValue2) {
oValue2 = _Helper.encodeURL(String(oValue2));
}
if (!bCaseSensitive && sType === "Edm.String") {
sPath = "toupper(" + sPath + ")";
}
switch (sOperator) {
case "EQ":
case "NE":
case "GT":
case "GE":
case "LT":
case "LE":
return sPath + "%20" + sOperator.toLowerCase() + "%20" + oValue1;
case "BT":
return "(" + sPath + "%20ge%20" + oValue1 + "%20and%20" + sPath + "%20le%20" + oValue2 + ")";
case "NB":
return "not%20(" + sPath + "%20ge%20" + oValue1 + "%20and%20" + sPath + "%20le%20" + oValue2 + ")";
case "Contains":
return "substringof(" + oValue1 + "," + sPath + ")";
case "NotContains":
return "not%20substringof(" + oValue1 + "," + sPath + ")";
case "StartsWith":
return "startswith(" + sPath + "," + oValue1 + ")";
case "NotStartsWith":
return "not%20startswith(" + sPath + "," + oValue1 + ")";
case "EndsWith":
return "endswith(" + sPath + "," + oValue1 + ")";
case "NotEndsWith":
return "not%20endswith(" + sPath + "," + oValue1 + ")";
default:
Log.error("Unknown filter operator '" + sOperator + "'", undefined, sClassName);
return "true";
}
};
/**
* Formats a JavaScript value according to the given
* <a href="http://www.odata.org/documentation/odata-version-2-0/overview#AbstractTypeSystem">
* EDM type</a>.
*
* @param {any} vValue The value to format
* @param {string} sType The EDM type (e.g. Edm.Decimal)
* @param {boolean} bCaseSensitive Whether strings gets compared case sensitive or not
* @return {string} The formatted value
* @public
*/
ODataUtils.formatValue = function(vValue, sType, bCaseSensitive) {
return ODataUtils._formatValue(vValue, sType, bCaseSensitive);
};
/**
* Like {@link #formatValue}, but allows for providing fractional seconds for Date values and if the given type
* is "Edm.DateTime" or "Edm.DateTimeOffset".
*
* @param {any} vValue The value to format
* @param {string} sType The EDM type (e.g. Edm.Decimal)
* @param {boolean} [bCaseSensitive=true] Whether strings gets compared case sensitive or not
* @param {string} [sFractionalSeconds] The fractional seconds to be appended to the given value in case it is a
* <code>Date<code>
* @return {string} The formatted value
* @private
*/
ODataUtils._formatValue = function(vValue, sType, bCaseSensitive, sFractionalSeconds) {
var oDate, sValue;
if (bCaseSensitive === undefined) {
bCaseSensitive = true;
}
// null values should return the null literal
if (vValue === null || vValue === undefined) {
return "null";
}
setDateTimeFormatter();
// Format according to the given type
switch (sType) {
case "Edm.String":
// quote
vValue = bCaseSensitive ? vValue : vValue.toUpperCase();
sValue = "'" + String(vValue).replace(/'/g, "''") + "'";
break;
case "Edm.Time":
if (typeof vValue === "object") {
// no need to use UI5Date.getInstance as only the UTC timestamp is used
sValue = oTimeFormat.format(new Date(vValue.ms), true);
} else {
sValue = "time'" + vValue + "'";
}
break;
case "Edm.DateTime":
// no need to use UI5Date.getInstance as only the UTC timestamp is used
oDate = vValue instanceof Date ? vValue : new Date(vValue);
if (oDate.getMilliseconds() > 0) {
sValue = oDateTimeFormatMs.format(oDate, true);
if (sFractionalSeconds) {
sValue = sValue.replace(rTrailingSingleQuote, sFractionalSeconds + "'");
}
} else {
sValue = oDateTimeFormat.format(oDate, true);
if (sFractionalSeconds) {
sValue = sValue.replace(rTrailingSingleQuote, ".000" + sFractionalSeconds + "'");
}
}
break;
case "Edm.DateTimeOffset":
// no need to use UI5Date.getInstance as only the UTC timestamp is used
oDate = vValue instanceof Date ? vValue : new Date(vValue);
if (oDate.getMilliseconds() > 0) {
sValue = oDateTimeOffsetFormatMs.format(oDate, true);
if (sFractionalSeconds) {
sValue = sValue.replace("Z'", sFractionalSeconds + "Z'");
}
} else {
sValue = oDateTimeOffsetFormat.format(oDate, true);
if (sFractionalSeconds) {
sValue = sValue.replace("Z'", ".000" + sFractionalSeconds + "Z'");
}
}
break;
case "Edm.Guid":
sValue = "guid'" + vValue + "'";
break;
case "Edm.Decimal":
sValue = vValue + "m";
break;
case "Edm.Int64":
sValue = vValue + "l";
break;
case "Edm.Double":
sValue = vValue + "d";
break;
case "Edm.Float":
case "Edm.Single":
sValue = vValue + "f";
break;
case "Edm.Binary":
sValue = "binary'" + vValue + "'";
break;
default:
sValue = String(vValue);
break;
}
return sValue;
};
/**
* Parses a given Edm type value to a value as it is stored in the
* {@link sap.ui.model.odata.v2.ODataModel}. The value to parse must be a valid Edm type literal
* as defined in chapter 2.2.2 "Abstract Type System" of the OData V2 specification.
*
* @param {string} sValue The value to parse
* @return {any} The parsed value
* @throws {Error} If the given value is not of an Edm type defined in the specification
* @private
*/
ODataUtils.parseValue = function (sValue) {
var sFirstChar = sValue[0],
sLastChar = sValue[sValue.length - 1];
setDateTimeFormatter();
if (sFirstChar === "'") { // Edm.String
return sValue.slice(1, -1).replace(/''/g, "'");
} else if (sValue.startsWith("time'")) { // Edm.Time
return {
__edmType : "Edm.Time",
ms : oTimeFormat.parse(sValue, true).getTime()
};
} else if (sValue.startsWith("datetime'")) { // Edm.DateTime
return sValue.includes(".")
? oDateTimeFormatMs.parse(sValue, true)
: oDateTimeFormat.parse(sValue, true);
} else if (sValue.startsWith("datetimeoffset'")) { // Edm.DateTimeOffset
return sValue.includes(".")
? oDateTimeOffsetFormatMs.parse(sValue, true)
: oDateTimeOffsetFormat.parse(sValue, true);
} else if (sValue.startsWith("guid'")) { // Edm.Guid
return sValue.slice(5, -1);
} else if (sValue === "null") { // null
return null;
} else if (sLastChar === "m" || sLastChar === "l" // Edm.Decimal, Edm.Int64
|| sLastChar === "d" || sLastChar === "f") { // Edm.Double, Edm.Single
return sValue.slice(0, -1);
} else if (!isNaN(sFirstChar) || sFirstChar === "-") { // Edm.Byte, Edm.Int16/32, Edm.SByte
return parseInt(sValue);
} else if (sValue === "true" || sValue === "false") { // Edm.Boolean
return sValue === "true";
} else if (sValue.startsWith("binary'")) { // Edm.Binary
return sValue.slice(7, -1);
}
throw new Error("Cannot parse value '" + sValue + "', no Edm type found");
};
/**
* Compares the given values using <code>===</code> and <code>></code>.
*
* @param {any} vValue1
* the first value to compare
* @param {any} vValue2
* the second value to compare
* @return {int}
* the result of the compare: <code>0</code> if the values are equal, <code>-1</code> if the
* first value is smaller, <code>1</code> if the first value is larger, <code>NaN</code> if
* they cannot be compared
*/
function simpleCompare(vValue1, vValue2) {
if (vValue1 === vValue2) {
return 0;
}
if (vValue1 === null || vValue2 === null
|| vValue1 === undefined || vValue2 === undefined) {
return NaN;
}
return vValue1 > vValue2 ? 1 : -1;
}
/**
* Parses a decimal given in a string.
*
* @param {string} sValue
* the value
* @returns {object}
* the result with the sign in <code>sign</code>, the number of integer digits in
* <code>integerLength</code> and the trimmed absolute value in <code>abs</code>
*/
function parseDecimal(sValue) {
var aMatches;
if (typeof sValue !== "string") {
return undefined;
}
aMatches = rDecimal.exec(sValue);
if (!aMatches) {
return undefined;
}
return {
sign: aMatches[1] === "-" ? -1 : 1,
integerLength: aMatches[2].length,
// remove trailing decimal zeroes and poss. the point afterwards
abs: aMatches[2] + aMatches[3].replace(rTrailingZeroes, "")
.replace(rTrailingDecimal, "")
};
}
/**
* Compares two decimal values given as strings.
*
* @param {string} sValue1
* the first value to compare
* @param {string} sValue2
* the second value to compare
* @return {int}
* the result of the compare: <code>0</code> if the values are equal, <code>-1</code> if the
* first value is smaller, <code>1</code> if the first value is larger, <code>NaN</code> if
* they cannot be compared
*/
function decimalCompare(sValue1, sValue2) {
var oDecimal1, oDecimal2, iResult;
if (sValue1 === sValue2) {
return 0;
}
oDecimal1 = parseDecimal(sValue1);
oDecimal2 = parseDecimal(sValue2);
if (!oDecimal1 || !oDecimal2) {
return NaN;
}
if (oDecimal1.sign !== oDecimal2.sign) {
return oDecimal1.sign > oDecimal2.sign ? 1 : -1;
}
// So they have the same sign.
// If the number of integer digits equals, we can simply compare the strings
iResult = simpleCompare(oDecimal1.integerLength, oDecimal2.integerLength)
|| simpleCompare(oDecimal1.abs, oDecimal2.abs);
return oDecimal1.sign * iResult;
}
var rTime = /^PT(\d\d)H(\d\d)M(\d\d)S$/;
/**
* Extracts the milliseconds if the value is a date/time instance or formatted string.
* @param {any} vValue
* the value (may be <code>undefined</code> or <code>null</code>)
* @returns {any}
* the number of milliseconds or the value itself
*/
function extractMilliseconds(vValue) {
if (typeof vValue === "string" && rTime.test(vValue)) {
vValue = parseInt(RegExp.$1) * 3600000 +
parseInt(RegExp.$2) * 60000 +
parseInt(RegExp.$3) * 1000;
}
if (vValue instanceof Date) {
return vValue.getTime();
}
if (vValue && vValue.__edmType === "Edm.Time") {
return vValue.ms;
}
return vValue;
}
/**
* Compares the given OData values based on their type. All date and time types can also be
* compared with a number. This number is then interpreted as the number of milliseconds that
* the corresponding date or time object should hold.
*
* @param {any} vValue1
* the first value to compare
* @param {any} vValue2
* the second value to compare
* @param {boolean} [bAsDecimal=false]
* if <code>true</code>, the string values <code>vValue1</code> and <code>vValue2</code> are
* compared as a decimal number (only sign, integer and fraction digits; no exponential
* format). Otherwise they are recognized by looking at their types.
* @return {int}
* the result of the compare: <code>0</code> if the values are equal, <code>-1</code> if the
* first value is smaller, <code>1</code> if the first value is larger, <code>NaN</code> if
* they cannot be compared
* @since 1.29.1
* @public
*/
ODataUtils.compare = function (vValue1, vValue2, bAsDecimal) {
return bAsDecimal ? decimalCompare(vValue1, vValue2)
: simpleCompare(extractMilliseconds(vValue1), extractMilliseconds(vValue2));
};
/**
* Returns a comparator function optimized for the given EDM type.
*
* @param {string} sEdmType
* the EDM type
* @returns {function(any, any): int}
* the comparator function taking two values of the given type and returning <code>0</code>
* if the values are equal, <code>-1</code> if the first value is smaller, <code>1</code> if
* the first value is larger and <code>NaN</code> if they cannot be compared (e.g. one value
* is <code>null</code> or <code>undefined</code>)
* @since 1.29.1
* @public
*/
ODataUtils.getComparator = function (sEdmType) {
switch (sEdmType) {
case "Edm.Date":
case "Edm.DateTime":
case "Edm.DateTimeOffset":
case "Edm.Time":
return ODataUtils.compare;
case "Edm.Decimal":
case "Edm.Int64":
return decimalCompare;
default:
return simpleCompare;
}
};
/**
* Normalizes the given canonical key.
*
* Although keys contained in OData response must be canonical, there are
* minor differences (like capitalization of suffixes for Decimal, Double,
* Float) which can differ and cause equality checks to fail.
*
* @param {string} sKey The canonical key of an entity
* @returns {string} Normalized key of the entry
* @protected
*/
// Define regular expression and function outside function to avoid instantiation on every call
var rNormalizeString = /([(=,])('.*?')([,)])/g,
rNormalizeCase = /[MLDF](?=[,)](?:[^']*'[^']*')*[^']*$)/g,
rNormalizeBinary = /([(=,])(X')/g,
fnNormalizeString = function(value, p1, p2, p3) {
return p1 + encodeURIComponent(decodeURIComponent(p2)) + p3;
},
fnNormalizeCase = function(value) {
return value.toLowerCase();
},
fnNormalizeBinary = function(value, p1) {
return p1 + "binary'";
};
ODataUtils._normalizeKey = function(sKey) {
return sKey.replace(rNormalizeString, fnNormalizeString).replace(rNormalizeCase, fnNormalizeCase).replace(rNormalizeBinary, fnNormalizeBinary);
};
/**
* Merges the given intervals into a single interval. The start and end of the resulting
* interval are the start of the first interval and the end of the last interval.
*
* @param {object[]} aIntervals
* The array of available intervals
* @returns {object|undefined}
* The merged interval with a member <code>start</code> and <code>end</code>, or
* <code>undefined</code> if no intervals are given.
*
* @private
*/
ODataUtils._mergeIntervals = function (aIntervals) {
if (aIntervals.length) {
return {start : aIntervals[0].start, end : aIntervals[aIntervals.length - 1].end};
}
return undefined;
};
/**
* Returns the array of gaps in the given array of elements, taking the given start index,
* length, and prefetch length into consideration.
*
* @param {any[]} aElements
* The array of available elements; it is used read-only to check if an element at a given
* index is not yet available (that is, is <code>undefined</code>)
* @param {number} iStart
* The start index of the range
* @param {number} iLength
* The length of the range; <code>Infinity</code> is supported
* @param {number} iPrefetchLength
* The number of elements to read before and after the given range; with this it is possible
* to prefetch data for a paged access. The read intervals are computed so that at least half
* the prefetch length is available left and right of the requested range without a further
* request. If data is missing on one side, the full prefetch length is added at this side.
* <code>Infinity</code> is supported.
* @param {number} [iLimit=Infinity]
* An upper limit on the number of elements
* @returns {object[]}
* Array of right open intervals which need to be read; each interval is an object with
* properties <code>start</code> and <code>end</code> with the interval's start and end index;
* empty if no intervals need to be read
*
* @private
* @see sap.ui.model.ListBinding#getContexts
*/
ODataUtils._getReadIntervals = function (aElements, iStart, iLength, iPrefetchLength, iLimit) {
var i, iEnd, n,
iGapStart = -1,
aIntervals = [],
oRange = ODataUtils._getReadRange(aElements, iStart, iLength, iPrefetchLength);
if (iLimit === undefined) {
iLimit = Infinity;
}
iEnd = Math.min(oRange.start + oRange.length, iLimit);
n = Math.min(iEnd, Math.max(oRange.start, aElements.length) + 1);
for (i = oRange.start; i < n; i += 1) {
if (aElements[i] !== undefined) {
if (iGapStart >= 0) {
aIntervals.push({start : iGapStart, end : i});
iGapStart = -1;
}
} else if (iGapStart < 0) {
iGapStart = i;
}
}
if (iGapStart >= 0) {
aIntervals.push({start : iGapStart, end : iEnd});
}
return aIntervals;
};
/**
* Calculates the index range to be read for the given start, length and prefetch length.
* Checks if <code>aElements</code> entries are available for at least half the prefetch length
* left and right to it. If not, the full prefetch length is added to this side, starting at the first missing
* index.
*
* @param {any[]} aElements
* The array of available elements
* @param {number} iStart
* The start index for the data request
* @param {number} iLength
* The number of requested entries
* @param {number} iPrefetchLength
* The number of entries to prefetch before and after the given range; <code>Infinity</code>
* is supported
* @param {function(any):boolean} [fnIsMissing]
* A function determining whether the given element is missing although it is not <code>undefined</code>
* @returns {object}
* An object with a member <code>start</code> for the start index for the next read and
* <code>length</code> for the number of entries to be read
*
* @private
*/
ODataUtils._getReadRange = function (aElements, iStart, iLength, iPrefetchLength, fnIsMissing) {
// Returns the index of the first missing element in the element range iFrom (inclusive) to iTo (exclusive) or
// -1 if no element is missing in the range
function getFirstMissingIndex(iFrom, iTo) {
const iStep = Math.sign(iTo - iFrom);
for (let i = iFrom; i !== iTo; i += iStep) {
if (aElements[i] === undefined || fnIsMissing?.(aElements[i])) {
return i;
}
}
return -1;
}
// Make sure that "half the prefetch length" is an integer. Round it up so that at least the
// half is checked on both sides. (With a prefetch of 5 for example, 3 elements are checked
// both to the left and to the right.)
const iHalfPrefetchLength = Math.ceil(iPrefetchLength / 2);
let iFirstMissingIndex = getFirstMissingIndex(iStart + iLength, iStart + iLength + iHalfPrefetchLength);
if (iFirstMissingIndex !== -1) {
const iAvailableElements = iFirstMissingIndex - (iStart + iLength);
iLength += iAvailableElements + iPrefetchLength;
}
// for start index 0, both iFrom and iTo passed to getFirstMissingIndex are -1, so that it returns -1
iFirstMissingIndex = getFirstMissingIndex(iStart - 1, Math.max(iStart - 1 - iHalfPrefetchLength, -1));
if (iFirstMissingIndex !== -1) {
const iAvailableElements = iStart - 1 - iFirstMissingIndex;
const iAdditionalElements = iAvailableElements + iPrefetchLength;
iLength += iAdditionalElements;
iStart -= iAdditionalElements;
if (iStart < 0) {
iLength += iStart; // Note: Infinity + -Infinity === NaN
if (isNaN(iLength)) {
iLength = Infinity;
}
iStart = 0;
}
}
return {length : iLength, start : iStart};
};
/**
* Removes all occurrences of the origin segment parameters <code>o</code> and <code>mo</code> from the given
* service URL.
*
* @param {string} sServiceURL
* The OData service base URI without query options as returned by
* {@link sap.ui.model.odata.v2.ODataModel#getServiceUrl} or
* {@link sap.ui.model.odata.v4.ODataModel#getServiceUrl}
* @returns {string}
* The OData service base URI without the origin segment parameters
*
* @private
* @see sap.ui.model.odata.ODataUtils.setOrigin
* @ui5-restricted sap.ui.fl
*/
ODataUtils.removeOriginSegmentParameters = function (sServiceURL) {
return sServiceURL.replaceAll(/(;o=[^\/;]+|;mo)(?=\/|;|$)/g, "");
};
return ODataUtils;
}, /* bExport= */ true);