@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
947 lines (864 loc) • 31.8 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 */
/**
* 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/security/encodeURL",
"sap/base/util/each",
"sap/ui/core/CalendarType",
"sap/ui/core/format/DateFormat",
"sap/ui/model/FilterProcessor",
"sap/ui/model/Sorter"
], function(assert, Log, encodeURL, each, CalendarType, DateFormat, FilterProcessor, Sorter) {
"use strict";
var oDateTimeFormat,
oDateTimeFormatMs,
oDateTimeOffsetFormat,
rDecimal = /^([-+]?)0*(\d+)(\.\d+|)$/,
// URL might be encoded, "(" becomes %28
rSegmentAfterCatalogService = /\/(Annotations|ServiceNames|ServiceCollection)(\(|%28)/,
oTimeFormat,
rTrailingDecimal = /\.$/,
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
});
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 URL parameters strings for filtering.
* The Parameter string is prepended with the "$filter=" system query option to form
* a valid URL part for OData Request.
* In case an array of filters is passed, they will be grouped in a way that filters on the
* same path are ORed and filters on different paths are ANDed with each other
* @see ODataUtils._createFilterParams
* @param {sap.ui.model.Filter|sap.ui.model.Filter[]} vFilter the root filter or filter array
* @param {object} oMetadata the entity metadata object
* @param {object} oEntityType the entity type object
* @return {string} the URL encoded filter parameters
* @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 will be used as URL query parameters for filtering.
* @param {sap.ui.model.Filter|sap.ui.model.Filter[]} vFilter the root filter or filter array
* @param {object} oMetadata the entity metadata object
* @param {object} oEntityType the entity type object
* @return {string} the URL encoded filter parameters
* @private
*/
ODataUtils._createFilterParams = function(vFilter, oMetadata, oEntityType) {
var that = this,
oFilter = Array.isArray(vFilter) ? FilterProcessor.groupFilters(vFilter) : vFilter;
function create(oFilter, bOmitBrackets) {
oFilter = convertLegacyFilter(oFilter);
if (oFilter.aFilters) {
return createMulti(oFilter, bOmitBrackets);
}
return that._createFilterSegment(oFilter.sPath, oMetadata, oEntityType, oFilter.sOperator, oFilter.oValue1, oFilter.oValue2, oFilter.bCaseSensitive);
}
function createMulti(oMultiFilter, bOmitBrackets) {
var aFilters = oMultiFilter.aFilters,
bAnd = !!oMultiFilter.bAnd,
sFilter = "";
if (aFilters.length === 0) {
return bAnd ? "true" : "false";
}
if (aFilters.length === 1) {
if (aFilters[0]._bMultiFilter) {
return create(aFilters[0]);
}
return create(aFilters[0], true);
}
if (!bOmitBrackets) {
sFilter += "(";
}
sFilter += create(aFilters[0]);
for (var i = 1; i < aFilters.length; i++) {
sFilter += bAnd ? "%20and%20" : "%20or%20";
sFilter += create(aFilters[i]);
}
if (!bOmitBrackets) {
sFilter += ")";
}
return sFilter;
}
if (!oFilter) {
return undefined;
}
return create(oFilter, true);
};
/**
* 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 {string} vParameters.force setting this flag to 'true' 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 metadata
* @param {object} oEntityType The entity type to filter
* @returns {string} A filter string
*
* @private
*/
ODataUtils._resolveMultiFilter = function(oMultiFilter, oMetadata, oEntityType){
var that = this,
aFilters = oMultiFilter.aFilters,
sFilterParam = "";
if (aFilters) {
sFilterParam += "(";
each(aFilters, function(i, oFilter) {
if (oFilter._bMultiFilter) {
sFilterParam += that._resolveMultiFilter(oFilter, oMetadata, oEntityType);
} else if (oFilter.sPath) {
sFilterParam += that._createFilterSegment(oFilter.sPath, oMetadata, oEntityType, oFilter.sOperator, oFilter.oValue1, oFilter.oValue2, "", oFilter.bCaseSensitive);
}
if (i < (aFilters.length - 1)) {
if (oMultiFilter.bAnd) {
sFilterParam += "%20and%20";
} else {
sFilterParam += "%20or%20";
}
}
});
sFilterParam += ")";
}
return sFilterParam;
};
/**
* Create a single filter segment of the OData filter parameters.
*
* @param {string} sPath The path to the value
* @param {sap.ui.model.odata.ODataMetadata} oMetadata The metadata
* @param {object} oEntityType The value's entity type
* @param {string} sOperator The filter operator
* @param {object} oValue1 The first value
* @param {object} oValue2 The second value
* @param {boolean} [bCaseSensitive=true] Whether the case should be considered
* @returns {string} The encoded string representation of the given filter
*
* @private
*/
ODataUtils._createFilterSegment = function(sPath, oMetadata, oEntityType, sOperator, oValue1, oValue2, bCaseSensitive) {
var oPropertyMetadata, sType;
if (bCaseSensitive === undefined) {
bCaseSensitive = true;
}
if (oEntityType) {
oPropertyMetadata = oMetadata._getPropertyMetadata(oEntityType, sPath);
sType = oPropertyMetadata && oPropertyMetadata.type;
assert(oPropertyMetadata, "PropertyType for property " + sPath + " of EntityType " + oEntityType.name + " not found!");
}
if (sType) {
oValue1 = this.formatValue(oValue1, sType, bCaseSensitive);
oValue2 = (oValue2 != null) ? this.formatValue(oValue2, sType, bCaseSensitive) : null;
} else {
assert(null, "Type for filter property could not be found in metadata!");
}
if (oValue1) {
oValue1 = encodeURL(String(oValue1));
}
if (oValue2) {
oValue2 = 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("ODataUtils :: Unknown filter operator " + sOperator);
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) {
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") {
sValue = oTimeFormat.format(new Date(vValue.ms), true);
} else {
sValue = "time'" + vValue + "'";
}
break;
case "Edm.DateTime":
oDate = vValue instanceof Date ? vValue : new Date(vValue);
if (oDate.getMilliseconds() > 0) {
sValue = oDateTimeFormatMs.format(oDate, true);
} else {
sValue = oDateTimeFormat.format(oDate, true);
}
break;
case "Edm.DateTimeOffset":
oDate = vValue instanceof Date ? vValue : new Date(vValue);
sValue = oDateTimeOffsetFormat.format(oDate, true);
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
if (sValue.indexOf(".") === -1) {
return oDateTimeFormat.parse(sValue, true);
} else { // Edm.DateTime with ms
return oDateTimeFormatMs.parse(sValue, true);
}
} else if (sValue.startsWith("datetimeoffset'")) { // Edm.DateTimeOffset
return 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 {string} [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}
* 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 half the prefetch length left and
* right to it. If not, the full prefetch length is added to this side.
*
* @param {object[]} 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
* @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) {
// Checks whether aElements contains at least one <code>undefined</code> entry within the
// given start (inclusive) and end (exclusive).
function isDataMissing(iStart, iEnd) {
var i;
for (i = iStart; i < iEnd; i += 1) {
if (aElements[i] === undefined) {
return true;
}
}
return false;
}
if (isDataMissing(iStart + iLength, iStart + iLength + iPrefetchLength / 2)) {
iLength += iPrefetchLength;
}
if (isDataMissing(Math.max(iStart - iPrefetchLength / 2, 0), iStart)) {
iLength += iPrefetchLength;
iStart -= iPrefetchLength;
if (iStart < 0) {
iLength += iStart; // Note: Infinity + -Infinity === NaN
if (isNaN(iLength)) {
iLength = Infinity;
}
iStart = 0;
}
}
return {length : iLength, start : iStart};
};
return ODataUtils;
}, /* bExport= */ true);