@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,228 lines (1,148 loc) • 146 kB
JavaScript
/*
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides class sap.ui.core.util.MockServer for mocking a server
sap.ui
.define(
[
'jquery.sap.global',
'sap/ui/Device',
'sap/ui/base/ManagedObject',
'sap/ui/thirdparty/sinon',
'sap/base/Log',
'sap/base/util/isEmptyObject',
'jquery.sap.sjax'
],
function(jQuery, Device, ManagedObject, sinon, Log, isEmptyObject/*, jQuerySapSjax*/) {
"use strict";
if (Device.browser.msie) {
if (window.sinon.log) { // sinon has no version property, but 'log' was removed with 2.x)
sap.ui.requireSync("sap/ui/thirdparty/sinon-ie");
}
// sinon internally checks the transported data to be an instance
// of FormData and this fails in case of IE9! - therefore we
// add a dummy function to enable instanceof check
if (!window.FormData) {
window.FormData = function() {};
}
}
/**
* Creates a mocked server. This helps to mock all or some back-end calls, e.g. for OData V2/JSON Models or simple XHR calls, without
* changing the application code. This class can also be used for qunit tests.
*
* @param {string} [sId] id for the new server object; generated automatically if no non-empty id is given
* Note: this can be omitted, no matter whether <code>mSettings</code> will be given or not!
* @param {object} [mSettings] optional map/JSON-object with initial property values, aggregated objects etc. for the new object
* @param {object} [oScope] scope object for resolving string based type and formatter references in bindings
*
* @class Class to mock http requests made to a remote server supporting the OData V2 REST protocol.
* @extends sap.ui.base.ManagedObject
* @abstract
* @author SAP SE
* @version 1.87.1
* @public
* @alias sap.ui.core.util.MockServer
*/
var MockServer = ManagedObject.extend("sap.ui.core.util.MockServer", /** @lends sap.ui.core.util.MockServer.prototype */ {
constructor: function(sId, mSettings, oScope) {
ManagedObject.apply(this, arguments);
MockServer._aServers.push(this);
},
metadata: {
library: "sap.ui.core",
properties: {
/**
* Setter for property <code>rootUri</code>. All request path URI are prefixed with this root URI if set.
*
* Default value is empty/<code>undefined</code>
* @param {string} rootUri new value for property <code>rootUri</code>
* @public
* @name sap.ui.core.util.MockServer#setRootUri
* @function
*/
/**
* Getter for property <code>rootUri</code>. Has to be relative and requires a trailing '/'. It also needs to match the URI set in OData/JSON models or simple XHR calls in order for the mock server to intercept them.
*
* Default value is empty/<code>undefined</code>.
* Must end with a a trailing slash ("/").
* @return {string} the value of property <code>rootUri</code>
* @public
* @name sap.ui.core.util.MockServer#getRootUri
* @function
*/
rootUri: "string",
/**
* Setter for property <code>recordRequests</code>. Defines whether or not the requests performed should be recorded (stored).
*
* Default value is <code>true</code>
* @param {boolean} recordRequests new value for property <code>recordRequests</code>
* @public
* @name sap.ui.core.util.MockServer#setRecordRequests
* @function
*/
/**
* Getter for property <code>recordRequests</code>. Returns whether or not the requests performed should be recorded (stored).
*
* Default value is <code>true</code>
*
* @return {boolean} the value of property <code>recordRequests</code>
* @public
* @name sap.ui.core.util.MockServer#getRecordRequests
* @function
*/
/**
* Whether or not the requests performed should be recorded (stored).
* This could be memory intense if each request is recorded.
* For unit testing purposes it should be set to <code>true</code> to compare requests performed
* otherwise this flag should be set to <code>false</code> e.g. for demonstration/app purposes.
*/
recordRequests: {type : "boolean", defaultValue : true},
/**
* Setter for property <code>requests</code>.
*
* Default value is is <code>[]</code>
*
* Each array entry should consist of an object with the following properties / values:
*
* <ul>
* <li><b>method <string>: "GET"|"POST"|"DELETE|"PUT"</b>
* <br>
* (any HTTP verb)
* </li>
* <li><b>path <string>: "/path/to/resource"</b>
* <br>
* The path is converted to a regular expression, so it can contain normal regular expression syntax.
* All regular expression groups are forwarded as arguments to the <code>response</code> function.
* In addition to this, parameters can be written in this notation: <code>:param</code>. These placeholder will be replaced by regular expression groups.
* </li>
* <li><b>response <function>: function(xhr, param1, param2, ...) { }</b>
* <br>
* The xhr object can be used to respond on the request. Supported methods are:
* <br>
* <code>xhr.respond(iStatusCode, mHeaders, sBody)</code>
* <br>
* <code>xhr.respondJSON(iStatusCode, mHeaders, oJsonObjectOrString)</code>. By default a JSON header is set for response header
* <br>
* <code>xhr.respondXML(iStatusCode, mHeaders, sXmlString)</code>. By default an XML header is set for response header
* <br>
* <code>xhr.respondFile(iStatusCode, mHeaders, sFileUrl)</code>. By default the mime type of the file is set for response header
* </li>
* </ul>
*
* @param {object[]} requests new value for property <code>requests</code>
* @public
* @name sap.ui.core.util.MockServer#setRequests
* @function
*/
/**
* Getter for property <code>requests</code>.
*
* Default value is <code>[]</code>
*
* @return {object[]} the value of property <code>rootUri</code>
* @public
* @name sap.ui.core.util.MockServer#getRequests
* @function
*/
requests: {
type: "object[]",
defaultValue: []
}
}
},
_oServer: null,
_aFilter: null,
_oMockdata: null,
_oMetadata: null,
_sMetadataUrl: null,
_sMockdataBaseUrl: null,
_mEntitySets: null,
_oErrorMessages: {
INVALID_SYSTEM_QUERY_OPTION_VALUE: "Invalid system query options value",
IS_NOT_A_VALID_SYSTEM_QUERY_OPTION: "## is not a valid system query option",
URI_VIOLATING_CONSTRUCTION_RULES: "The URI is violating the construction rules defined in the Data Services specification",
UNSUPPORTED_FORMAT_VALUE: "Unsupported format value. Only json format is supported",
MALFORMED_SYNTAX: "The Data Services Request could not be understood due to malformed syntax",
RESOURCE_NOT_FOUND: "Resource not found",
INVALID_SORTORDER_DETECTED: "Invalid sortorder ## detected",
PROPERTY_NOT_FOUND: "Property ## not found",
INVALID_FILTER_QUERY_STATEMENT: "Invalid filter query statement",
INVALID_FILTER_OPERATOR: "Invalid $filter operator ##",
RESOURCE_NOT_FOUND_FOR_SEGMENT: "Resource not found for the segment ##",
MALFORMED_URI_LITERAL_SYNTAX_IN_KEY: "Malformed URI literal syntax in key ##",
INVALID_KEY_NAME: "Invalid key name in key predicate. Expected name is ##",
INVALID_KEY_PREDICATE_QUANTITY: "Invalid key predicate. The quantity of provided keys does not match the expected value",
INVALID_KEY_TYPE: "Invalid key predicate. The key literal for key property ## does not match its type."
},
_oRandomSeed: {}
});
/**
* Generates a floating-point, pseudo-random number in the range [0, 1[
* using a linear congruential generator with drand48 parameters
* the seed is fixed, so the generated random sequence is always the same
* each property type has a own seed. Valid types are:
* String, DateTime, Int, Decimal, Boolean, Byte, Double, Single, SByte, Time, Guid, Binary, DateTimeOffset
* @private
* @param {string} specific property type of random mock value to be generated
* @return (number) pseudo-random number
*/
MockServer.prototype._getPseudoRandomNumber = function (sType) {
if (!this._oRandomSeed) {
this._oRandomSeed = {};
}
if (!this._oRandomSeed.hasOwnProperty(sType)) {
this._oRandomSeed[sType] = 0;
}
this._oRandomSeed[sType] = (this._oRandomSeed[sType] + 11 ) * 25214903917 % 281474976710655;
return this._oRandomSeed[sType] / 281474976710655;
};
/**
* reset seed of pseudo-random number generator
* @private
*/
MockServer.prototype._resetPseudoRandomNumberGenerator = function () {
this._oRandomSeed = {};
};
/**
* Starts the server.
* @public
*/
MockServer.prototype.start = function() {
this._oServer = MockServer._getInstance();
this._aFilters = [];
var aRequests = this.getRequests();
var that = this;
aRequests.forEach(function(oRequest) {
var fnResponse;
if (that.getRecordRequests() === false && oRequest.response) {
fnResponse = function() {
oRequest.response.apply(this, arguments);
// reset recorded requests for memory savings as mockserver is also used for apps and not only testing
that._oServer.requests = [];
};
} else {
fnResponse = oRequest.response;
}
that._addRequestHandler(oRequest.method, oRequest.path, fnResponse);
});
};
/**
* Stops the server.
* @public
*/
MockServer.prototype.stop = function() {
if (this.isStarted()) {
this._removeAllRequestHandlers();
this._removeAllFilters();
this._oServer = null;
}
};
/**
* Attaches an event handler to be called before the built-in request processing of the mock server
* @param {string} event type according to HTTP Method
* @param {function} fnCallback - the name of the function that will be called at this exit.
* The callback function exposes an event with parameters, depending on the type of the request.
* oEvent.getParameters() lists the parameters as per the request. Examples are:
* oXhr : the request object; sUrlParams : the URL parameters of the request; sKeys : key properties of the requested entry; sNavProp/sNavName : name of navigation
* @param {string} sEntitySet - (optional) the name of the entity set
* @public
*/
MockServer.prototype.attachBefore = function(sHttpMethod, fnCallback, sEntitySet) {
sEntitySet = sEntitySet ? sEntitySet : "";
this.attachEvent(sHttpMethod + sEntitySet + ":before", fnCallback);
};
/**
* Attaches an event handler to be called after the built-in request processing of the mock server
* @param {string} event type according to HTTP Method
* @param {function} fnCallback - the name of the function that will be called at this exit
* The callback function exposes an event with parameters, depending on the type of the request.
* oEvent.getParameters() lists the parameters as per the request. Examples are:
* oXhr : the request object; oFilteredData : the mock data entries that are about to be returned in the response; oEntry : the mock data entry that is about to be returned in the response;
* @param {string} sEntitySet - (optional) the name of the entity set
* @public
*/
MockServer.prototype.attachAfter = function(sHttpMethod, fnCallback, sEntitySet) {
sEntitySet = sEntitySet ? sEntitySet : "";
this.attachEvent(sHttpMethod + sEntitySet + ":after", fnCallback);
};
/**
* Removes a previously attached event handler
* @param {string} event type according to HTTP Method
* @param {function} fnCallback - the name of the function that will be called at this exit
* @param {string} sEntitySet - (optional) the name of the entity set
* @public
*/
MockServer.prototype.detachBefore = function(sHttpMethod, fnCallback, sEntitySet) {
sEntitySet = sEntitySet ? sEntitySet : "";
this.detachEvent(sHttpMethod + sEntitySet + ":before", fnCallback);
};
/**
* Removes a previously attached event handler
* @param {string} event type according to HTTP Method
* @param {function} fnCallback - the name of the function that will be called at this exit
* @param {string} sEntitySet - (optional) the name of the entity set
* @public
*/
MockServer.prototype.detachAfter = function(sHttpMethod, fnCallback, sEntitySet) {
sEntitySet = sEntitySet ? sEntitySet : "";
this.detachEvent(sHttpMethod + sEntitySet + ":after", fnCallback);
};
/**
* Returns whether the server is started or not.
*
* @return {boolean} whether the server is started or not.
* @public
*/
MockServer.prototype.isStarted = function() {
return !!this._oServer;
};
/**
* Returns the data model of the given EntitySet name.
*
* @param {string} sEntitySetName EntitySet name
* @return {array} data model of the given EntitySet
* @public
*/
MockServer.prototype.getEntitySetData = function(sEntitySetName) {
var that = this;
var aCopiedMockdata;
if (this._oMockdata && this._oMockdata.hasOwnProperty(sEntitySetName)) {
aCopiedMockdata = jQuery.extend(true, [], that._oMockdata[sEntitySetName]);
} else {
Log.error("Unrecognized EntitySet name: " + sEntitySetName);
}
return aCopiedMockdata;
};
/**
* Sets the data of the given EntitySet name with the given array.
* @param {string} sEntitySetName EntitySet name
* @param {array} aData
* @public
*/
MockServer.prototype.setEntitySetData = function(sEntitySetName, aData) {
if (this._oMockdata && this._oMockdata.hasOwnProperty(sEntitySetName)) {
this._oMockdata[sEntitySetName] = aData;
} else {
Log.error("Unrecognized EntitySet name: " + sEntitySetName);
}
};
/**
* Applies the OData system query option string on the given array
* @param {object} oFilteredData
* @param {string} sQuery string in the form {query}={value}
* @param {string} sEntitySetName the name of the entitySet the oFilteredData belongs to
* @param {array} aUrlParamStrings all query string parts of the request (array of {query}={value})
* @private
*/
MockServer.prototype._applyQueryOnCollection = function(oFilteredData, sQuery, sEntitySetName, aUrlParamStrings) {
var aQuery = sQuery.split('=');
var sODataQueryValue = aQuery[1];
if (sODataQueryValue === "") {
return;
}
if (sODataQueryValue.lastIndexOf(',') === sODataQueryValue.length - 1) {
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.URI_VIOLATING_CONSTRUCTION_RULES);
}
switch (aQuery[0]) {
case "$top":
if (!(new RegExp(/^\d+$/).test(sODataQueryValue))) {
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.INVALID_SYSTEM_QUERY_OPTION_VALUE);
}
oFilteredData.results = oFilteredData.results.slice(0, sODataQueryValue);
break;
case "$skip":
if (!(new RegExp(/^\d+$/).test(sODataQueryValue))) {
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.INVALID_SYSTEM_QUERY_OPTION_VALUE);
}
oFilteredData.results = oFilteredData.results.slice(sODataQueryValue, oFilteredData.results.length);
break;
case "$orderby":
oFilteredData.results = this._getOdataQueryOrderby(oFilteredData.results, sODataQueryValue, sEntitySetName);
break;
case "$filter":
oFilteredData.results = this._recursiveOdataQueryFilter(oFilteredData.results, sODataQueryValue);
break;
case "search-focus":
// query parameter "search-focus" is evaluated together with parameter "search"
break;
case "search":
//Look for "search-focus" first...
var sSearchFocus = "";
for (var i = 0; i < aUrlParamStrings.length; i++ ) {
if ( aUrlParamStrings[i].indexOf("search-focus") != -1 ){
sSearchFocus = aUrlParamStrings[i].split('=')[1];
break;
}
}
oFilteredData.results = this._recursiveOdataQuerySearch( oFilteredData.results, sODataQueryValue, sSearchFocus, sEntitySetName );
break;
case "$select":
oFilteredData.results = this._getOdataQuerySelect(oFilteredData.results, sODataQueryValue, sEntitySetName);
break;
case "$inlinecount":
var iCount = this._getOdataInlineCount(oFilteredData.results, sODataQueryValue);
if (iCount) {
oFilteredData.__count = iCount;
}
break;
case "$expand":
oFilteredData.results = this._getOdataQueryExpand(oFilteredData.results, sODataQueryValue, sEntitySetName);
break;
case "$format":
oFilteredData.results = this._getOdataQueryFormat(oFilteredData.results, sODataQueryValue);
break;
default:
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.IS_NOT_A_VALID_SYSTEM_QUERY_OPTION, aQuery[0]);
}
};
/**
* Applies the OData system query option string on the given entry
* @param {object} oEntry
* @param {string} sQuery string of the form {query}={value}
* @param {string} sEntitySetName the name of the entitySet the oEntry belongs to
* @private
*/
MockServer.prototype._applyQueryOnEntry = function(oEntry, sQuery, sEntitySetName) {
var aQuery = sQuery.split('=');
var sODataQueryValue = aQuery[1];
if (sODataQueryValue === "") {
return;
}
if (sODataQueryValue.lastIndexOf(',') === sODataQueryValue.length - 1) {
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.URI_VIOLATING_CONSTRUCTION_RULES);
}
switch (aQuery[0]) {
case "$filter":
return this._recursiveOdataQueryFilter([oEntry], sODataQueryValue)[0];
case "$select":
return this._getOdataQuerySelect([oEntry], sODataQueryValue, sEntitySetName)[0];
case "$expand":
return this._getOdataQueryExpand([oEntry], sODataQueryValue, sEntitySetName)[0];
case "$format":
return this._getOdataQueryFormat([oEntry], sODataQueryValue);
default:
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.IS_NOT_A_VALID_SYSTEM_QUERY_OPTION, aQuery[0]);
}
};
/**
* Applies the Orderby OData system query option string on the given array
* @param {object} aDataSet
* @param {string} sODataQueryValue a comma separated list of property navigation paths to sort by, where each property navigation path terminates on a primitive property
* @private
*/
MockServer.prototype._getOdataQueryOrderby = function(aDataSet, sODataQueryValue, sEntitySetName) {
// sort properties lookup
var aProperties = sODataQueryValue.split(',');
var that = this;
//trim all properties
jQuery.each(aProperties, function(i, sPropertyName) {
aProperties[i] = that._trim(sPropertyName);
});
var fnComparator = function compare(a, b) {
for (var i = 0; i < aProperties.length; i++) {
// sort order lookup asc / desc
var aSort = aProperties[i].split(' ');
// by default the sort is in asc order
var iSorter = 1;
if (aSort.length > 1) {
switch (aSort[1]) {
case 'asc':
iSorter = 1;
break;
case 'desc':
iSorter = -1;
break;
default:
that._logAndThrowMockServerCustomError(400, that._oErrorMessages.INVALID_SORTORDER_DETECTED, aSort[1]);
}
}
// support for 1 level complex type property
var sPropName, sComplexType;
var iComplexType = aSort[0].indexOf("/");
if (iComplexType !== -1) {
sPropName = aSort[0].substring(iComplexType + 1);
sComplexType = aSort[0].substring(0, iComplexType);
if (!a[sComplexType].hasOwnProperty(sPropName)) {
var bExist = false;
var aTypeProperties = [];
if (sComplexType) {
var sTargetEntitySet = that._mEntitySets[sEntitySetName].navprops[sComplexType].to.entitySet;
aTypeProperties = that._mEntityTypes[that._mEntitySets[sTargetEntitySet].type].properties;
for (var i = 0; i < aTypeProperties.length; i++) {
if (aTypeProperties[i].name === sPropName) {
bExist = true;
break;
}
}
}
if (!bExist) {
that._logAndThrowMockServerCustomError(400, that._oErrorMessages.PROPERTY_NOT_FOUND, sPropName);
}
}
if (a[sComplexType][sPropName] < b[sComplexType][sPropName]) {
return -1 * iSorter;
}
if (a[sComplexType][sPropName] > b[sComplexType][sPropName]) {
return 1 * iSorter;
}
} else {
sPropName = aSort[0];
if (!a.hasOwnProperty(sPropName)) {
that._logAndThrowMockServerCustomError(400, that._oErrorMessages.PROPERTY_NOT_FOUND, sPropName);
}
if (a[sPropName] < b[sPropName]) {
return -1 * iSorter;
}
if (a[sPropName] > b[sPropName]) {
return 1 * iSorter;
}
}
}
return 0;
};
return aDataSet.sort(fnComparator);
};
/**
* Removes duplicate entries from the given array
* @param {object} aDataSet
* @private
*/
MockServer.prototype._arrayUnique = function(array) {
var a = array.concat();
for (var i = 0; i < a.length; ++i) {
for (var j = i + 1; j < a.length; ++j) {
if (a[i] === a[j]) {
a.splice(j--, 1);
}
}
}
return a;
};
/**
* Returns the indices of the first brackets appearance, excluding brackets of $filter reserved functions
* @param {string} sString
* @private
*/
MockServer.prototype._getBracketIndices = function(sString) {
var aStack = [];
var bReserved = false;
var iStartIndex, iEndIndex = 0;
for (var character = 0; character < sString.length; character++) {
if (sString[character] === '(') {
if (/[substringof|endswith|startswith]$/.test(sString.substring(0, character))) {
bReserved = true;
} else {
aStack.push(sString[character]);
if (iStartIndex === undefined) {
iStartIndex = character;
}
}
} else if (sString[character] === ')') {
if (!bReserved) {
aStack.pop();
iEndIndex = character;
if (aStack.length === 0) {
return {
start: iStartIndex,
end: iEndIndex
};
}
} else {
bReserved = false;
}
}
}
return {
start: iStartIndex,
end: iEndIndex
};
};
/**
* Applies the $filter OData system query option string on the given array.
* This function is called recursively on expressions in brackets.
* @param {string} sString
* @private
*/
MockServer.prototype._recursiveOdataQueryFilter = function(aDataSet, sODataQueryValue) {
// check for wrapping brackets, e.g. (A), (A op B), (A op (B)), (((A)))
var oIndices = this._getBracketIndices(sODataQueryValue);
if (oIndices.start === 0 && oIndices.end === sODataQueryValue.length - 1) {
sODataQueryValue = this._trim(sODataQueryValue.substring(oIndices.start + 1, oIndices.end));
return this._recursiveOdataQueryFilter(aDataSet, sODataQueryValue);
}
// find brackets that are not related to the reserved words
var rExp = /([^substringof|endswith|startswith]|^)\((.*)\)/,
aSet2,
aParts;
var sOperator;
if (rExp.test(sODataQueryValue)) {
var sBracketed = sODataQueryValue.substring(oIndices.start, oIndices.end + 1);
var rExp1 = new RegExp("(.*) +(or|and) +(" + this._trim(this._escapeStringForRegExp(sBracketed)) + ".*)");
if (oIndices.start === 0) {
rExp1 = new RegExp("(" + this._trim(this._escapeStringForRegExp(sBracketed)) + ") +(or|and) +(.*)");
}
var aExp1Parts = rExp1.exec(sODataQueryValue);
if (aExp1Parts === null) {
return this._getOdataQueryFilter(aDataSet, this._trim(sODataQueryValue));
}
var sExpression = aExp1Parts[1];
sOperator = aExp1Parts[2];
var sExpression2 = aExp1Parts[3];
var aSet1 = this._recursiveOdataQueryFilter(aDataSet, sExpression);
if (sOperator === "or") {
aSet2 = this._recursiveOdataQueryFilter(aDataSet, sExpression2);
return this._arrayUnique(aSet1.concat(aSet2));
}
if (sOperator === "and") {
return this._recursiveOdataQueryFilter(aSet1, sExpression2);
}
} else {
//there are only brackets with the reserved words
// e.g. A or B and C or D
aParts = sODataQueryValue.split(/ +and | or +/);
// base case
if (aParts.length === 1) {
// IE8 handling
if (sODataQueryValue.match(/ +and | or +/)) {
throw new Error("400");
}
return this._getOdataQueryFilter(aDataSet, this._trim(sODataQueryValue));
}
var aResult = this._recursiveOdataQueryFilter(aDataSet, aParts[0]);
var rRegExp;
for (var i = 1; i < aParts.length; i++) {
rRegExp = new RegExp(this._trim(this._escapeStringForRegExp(aParts[i - 1])) + " +(and|or) +" + this._trim(this._escapeStringForRegExp(
aParts[i])));
sOperator = rRegExp.exec(sODataQueryValue)[1];
if (sOperator === "or") {
aSet2 = this._recursiveOdataQueryFilter(aDataSet, aParts[i]);
aResult = this._arrayUnique(aResult.concat(aSet2));
}
if (sOperator === "and") {
aResult = this._recursiveOdataQueryFilter(aResult, aParts[i]);
}
}
return aResult;
}
};
/**
* Applies the Filter OData system query option string on the given array
* @param {object} aDataSet
* @param {string} sODataQueryValue a boolean expression
* @private
*/
MockServer.prototype._getOdataQueryFilter = function(aDataSet, sODataQueryValue) {
if (aDataSet.length === 0) {
return aDataSet;
}
var rExp = new RegExp("(.*) (eq|ne|gt|lt|le|ge) (.*)");
var rExp2 = new RegExp("(endswith|startswith|substringof)\\((.*)");
var sODataFilterMethod = null;
var aODataFilterValues = rExp.exec(sODataQueryValue);
if (aODataFilterValues) {
sODataFilterMethod = aODataFilterValues[2];
} else {
aODataFilterValues = rExp2.exec(sODataQueryValue);
if (aODataFilterValues) {
sODataFilterMethod = aODataFilterValues[1];
} else {
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.INVALID_FILTER_QUERY_STATEMENT);
}
}
var that = this;
var fnGetFilteredData = function(bValue, iValueIndex, iPathIndex, fnSelectFilteredData) {
var aODataFilterValues, sValue, sPath;
if (!bValue) { //e.g eq, ne, gt, lt, le, ge
aODataFilterValues = rExp.exec(sODataQueryValue);
sValue = that._trim(aODataFilterValues[iValueIndex + 1]);
sPath = that._trim(aODataFilterValues[iPathIndex + 1]);
} else { //e.g.substringof, startswith, endswith
var rStringFilterExpr = new RegExp("(substringof|startswith|endswith)\\(([^\\)]*),(.*)\\)");
aODataFilterValues = rStringFilterExpr.exec(sODataQueryValue);
sValue = that._trim(aODataFilterValues[iValueIndex + 2]);
sPath = that._trim(aODataFilterValues[iPathIndex + 2]);
}
// remove brackets around value (if value is present)
if (/^\(.+\)$/.test(sValue)) {
sValue = sValue.replace(/^\(|\)$/g, "");
}
//TODO do the check using the property type and not value
// remove number suffixes from EDM types decimal, Int64, Single
var sTypecheck = sValue[sValue.length - 1];
if (sTypecheck === "M" || sTypecheck === "m" || sTypecheck === "L" || sTypecheck === "f") {
sValue = sValue.substring(0, sValue.length - 1);
}
//fix for filtering on date time properties
if (sValue.indexOf("datetime") === 0) {
sValue = that._getJsonDate(sValue);
} else if (sValue.indexOf("guid") === 0) {
// strip the "guid'" (5) from the front and the "'" (-1) from the back
sValue = sValue.substring(5, sValue.length - 1);
} else if (sValue === "true") { // fix for filtering on boolean properties
sValue = true;
} else if (sValue === "false") {
sValue = false;
} else if (that._isValidNumber(sValue)) { //fix for filtering on properties of type number
sValue = parseFloat(sValue);
} else if ((sValue.charAt(0) === "'") && (sValue.charAt(sValue.length - 1) === "'")) {
//fix for filtering on properties of type string
sValue = sValue.substr(1, sValue.length - 2);
}
// support for 1 level complex type property
var iComplexType = sPath.indexOf("/");
if (iComplexType !== -1) {
var sPropName = sPath.substring(iComplexType + 1);
var sComplexType = sPath.substring(0, iComplexType);
if (aDataSet[0][sComplexType]) {
if (!aDataSet[0][sComplexType].hasOwnProperty(sPropName)) {
var sErrorMessage = that._oErrorMessages.PROPERTY_NOT_FOUND.replace("##", "'" + sPropName + "'");
Log.error("MockServer: navigation property '" + sComplexType + "' was not expanded, so " + sErrorMessage);
return aDataSet;
}
} else {
that._logAndThrowMockServerCustomError(400, that._oErrorMessages.PROPERTY_NOT_FOUND, sPath);
}
return fnSelectFilteredData(sPath, sValue, sComplexType, sPropName);
} else {
//check if sPath exists as property of the entityset
if (!aDataSet[0].hasOwnProperty(sPath)) {
that._logAndThrowMockServerCustomError(400, that._oErrorMessages.PROPERTY_NOT_FOUND, sPath);
}
return fnSelectFilteredData(sPath, sValue);
}
};
switch (sODataFilterMethod) {
case "substringof":
return fnGetFilteredData(true, 0, 1, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (typeof oMockData[sComplexType][sPropName] === "string" && oMockData[sComplexType][sPropName].indexOf(sValue) !== -1);
}
return (typeof oMockData[sPath] === "string" && oMockData[sPath].indexOf(sValue) !== -1);
});
});
case "startswith":
return fnGetFilteredData(true, 1, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (typeof oMockData[sComplexType][sPropName] === "string" && oMockData[sComplexType][sPropName].indexOf(sValue) === 0);
}
return (typeof oMockData[sPath] === "string" && oMockData[sPath].indexOf(sValue) === 0);
});
});
case "endswith":
return fnGetFilteredData(true, 1, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (typeof oMockData[sComplexType][sPropName] === "string" && oMockData[sComplexType][sPropName].indexOf(sValue) === (oMockData[sComplexType][sPropName].length - sValue.length));
}
return (typeof oMockData[sPath] === "string" && oMockData[sPath].indexOf(sValue) === (oMockData[sPath].length - sValue.length));
});
});
case "eq":
return fnGetFilteredData(false, 2, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (oMockData[sComplexType][sPropName] === sValue);
}
return (oMockData[sPath] === sValue);
});
});
case "ne":
return fnGetFilteredData(false, 2, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (oMockData[sComplexType][sPropName] !== sValue);
}
return (oMockData[sPath] !== sValue);
});
});
case "gt":
return fnGetFilteredData(false, 2, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (oMockData[sComplexType][sPropName] > sValue);
}
return (oMockData[sPath] > sValue);
});
});
case "lt":
return fnGetFilteredData(false, 2, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (oMockData[sComplexType][sPropName] < sValue);
}
return (oMockData[sPath] < sValue);
});
});
case "ge":
return fnGetFilteredData(false, 2, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (oMockData[sComplexType][sPropName] >= sValue);
}
return (oMockData[sPath] >= sValue);
});
});
case "le":
return fnGetFilteredData(false, 2, 0, function(sPath, sValue, sComplexType, sPropName) {
return aDataSet.filter(function(oMockData) {
if (sComplexType && sPropName) {
return (oMockData[sComplexType][sPropName] <= sValue);
}
return (oMockData[sPath] <= sValue);
});
});
default:
this._logAndThrowMockServerCustomError(400, that._oErrorMessages.INVALID_FILTER_OPERATOR, sODataFilterMethod);
}
};
/**
* Processes the search operation:
* Technically a filter is applied with "substringof" filter on the given property (given as
* search-focus URL parameter).
*
* @param {object} aDataSet
* @param {string} sODataQueryValue search string
* @param {string} sODataSearchFocusValue A property name on which entries should be searched
* @return {object} Changed result data set
*
* @private
*/
MockServer.prototype._recursiveOdataQuerySearch = function(aDataSet, sODataQueryValue, sODataSearchFocusValue, sEntitySetName) {
var sFilterString = "";
if ( sODataSearchFocusValue == "" || sODataSearchFocusValue == undefined ){
for ( var i = 0; i < this._mEntitySets[sEntitySetName].keys.length; i++ ) {
if (i != 0){
sFilterString = sFilterString + " or ";
}
sFilterString = sFilterString + "startswith(" + this._mEntitySets[sEntitySetName].keys[i] + ",'" + sODataQueryValue + "')";
}
} else {
sFilterString = "substringof('" + sODataQueryValue + "'," + sODataSearchFocusValue + ")";
}
return this._recursiveOdataQueryFilter(aDataSet, sFilterString);
};
/**
* Applies the Select OData system query option string on the given array
* @param {object} aDataSet
* @param {string} sODataQueryValue a comma separated list of property paths, qualified action names, qualified function names, or the star operator (*)
* @private
*/
MockServer.prototype._getOdataQuerySelect = function(aDataSet, sODataQueryValue, sEntitySetName) {
var that = this;
var sPropName, sComplexOrNavProperty;
var aProperties = sODataQueryValue.split(',');
var aSelectedDataSet = [];
var oPushedObject;
var oDataEntry = aDataSet[0] ? aDataSet[0][aProperties[0].split('/')[0]] : null;
if (!(oDataEntry != null && oDataEntry.results && oDataEntry.results.length > 0)) {
var fnCreatePushedEntry = function (aProperties, oData, oPushedObject, sParentName) {
// Get for each complex type or navigation property its list of properties
var oComplexOrNav = {};
jQuery.each(aProperties, function (i, sPropertyName) {
var iComplexOrNavProperty = sPropertyName.indexOf("/");
// This is a complex type or navigation property
if (iComplexOrNavProperty !== -1) {
sPropName = sPropertyName.substring(iComplexOrNavProperty + 1);
sComplexOrNavProperty = sPropertyName.substring(0, iComplexOrNavProperty);
if (oComplexOrNav[sComplexOrNavProperty]) {
oComplexOrNav[sComplexOrNavProperty].push(sPropName);
} else {
oComplexOrNav[sComplexOrNavProperty] = [sPropName];
}
}
});
jQuery.each(Object.keys(oComplexOrNav), function (i, sComplexOrNav) {
if (!oPushedObject[sComplexOrNav]) {
oPushedObject[sComplexOrNav] = {};
}
// call recursively to get the properties of each complex type or navigation property
oPushedObject[sComplexOrNav] = fnCreatePushedEntry(oComplexOrNav[sComplexOrNav], oData[sComplexOrNav], oPushedObject[sComplexOrNav], sComplexOrNav);
});
if (oData.results) {
// Navigation property - filter the results for each navigation property based on the properties defined by $select
var oFilteredResults = [];
jQuery.each(oData.results, function (i, oResult) {
var oFilteredResult = {};
jQuery.each(aProperties, function (j, sPropertyName) {
oFilteredResult[sPropertyName] = oResult[sPropertyName];
});
oFilteredResults.push(oFilteredResult);
});
if (oPushedObject) {
oPushedObject.results = oFilteredResults;
}
} else {
// Complex types or flat properties
if (oData["__metadata"]) {
oPushedObject["__metadata"] = oData["__metadata"];
}
jQuery.each(aProperties, function (i, sPropertyName) {
var iComplexType = sPropertyName.indexOf("/");
if (iComplexType === -1) { // Complex types were already handled above
if (oData && !oData.hasOwnProperty(sPropertyName)) {
var bExist = false;
var aTypeProperties = [];
if (sParentName) {
var sTargetEntitySet = that._mEntitySets[sEntitySetName].navprops[sParentName].to.entitySet;
aTypeProperties = that._mEntityTypes[that._mEntitySets[sTargetEntitySet].type].properties;
for (var i = 0; i < aTypeProperties.length; i++) {
if (aTypeProperties[i].name === sPropertyName) {
bExist = true;
break;
}
}
}
if (!bExist) {
that._logAndThrowMockServerCustomError(404, that._oErrorMessages.RESOURCE_NOT_FOUND_FOR_SEGMENT, sPropertyName);
}
}
oPushedObject[sPropertyName] = oData[sPropertyName];
}
});
}
return oPushedObject;
};
// in case of $select=* return the data as is
if (aProperties.indexOf("*") !== -1) {
return aDataSet;
}
// trim all properties
jQuery.each(aProperties, function(i, sPropertyName) {
aProperties[i] = that._trim(sPropertyName);
});
// for each entry in the dataset create a new object that contains only the properties in $select clause
jQuery.each(aDataSet, function(iIndex, oData) {
oPushedObject = {};
aSelectedDataSet.push(fnCreatePushedEntry(aProperties, oData, oPushedObject));
});
} else {
//Add Support for multiple select return 1...n
var fnMultiSelect = function(data, select, currentPath) {
var result = {};
// traversed path to get to data:
currentPath = currentPath || '';
if (typeof data !== 'object') {
return data;
}
if (typeof data.slice === 'function') {
return data.map(function(el, index) {
return fnMultiSelect(el, select, currentPath); // on same path
});
}
// If Object:
// Handle "__metadata" property
if (data.__metadata !== undefined && currentPath.length === 0) {
result.__metadata = data.__metadata;
}
// Take the relevant paths only.
select.filter(function(path) {
return (path + '/').indexOf(currentPath) === 0;
}).forEach(function(path, _, innerSelect) {
// then get the next property in given path
var propertyKey = path.substr(currentPath.length).split('/')[0];
// Check if we have that propertyKey on the current object
if (data[propertyKey] !== undefined) {
// in this case recurse again while adding this to the current path
result[propertyKey] = fnMultiSelect(data[propertyKey], innerSelect, currentPath + propertyKey + '/');
}
});
// Add specific results case handling
if (data.results !== undefined) { // recurse with same path
result.results = fnMultiSelect(data.results, select, currentPath);
}
return result;
};
//invoke recursive function
aSelectedDataSet = fnMultiSelect(aDataSet, aProperties);
}
return aSelectedDataSet;
};
/**
* Applies the InlineCount OData system query option string on the given array
* @param {object} aDataSet
* @param {string} sODataQueryValue a value of allpages, or a value of none
* @private
*/
MockServer.prototype._getOdataInlineCount = function(aDataSet, sODataQueryValue) {
var aProperties = sODataQueryValue.split(',');
if (aProperties.length !== 1 || (aProperties[0] !== 'none' && aProperties[0] !== 'allpages')) {
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.INVALID_SYSTEM_QUERY_OPTION_VALUE);
}
if (aProperties[0] === 'none') {
return;
}
return aDataSet.length;
};
/**
* Applies the Format OData system query option
* @param {string} sODataQueryValue
* @private
*/
MockServer.prototype._getOdataQueryFormat = function(aDataSet, sODataQueryValue) {
if (sODataQueryValue !== 'json') {
this._logAndThrowMockServerCustomError(400, this._oErrorMessages.UNSUPPORTED_FORMAT_VALUE);
}
return aDataSet;
};
/**
* Applies the Expand OData system query option string on the given array
* @param {object} aDataSet
* @param {string} sODataQueryValue a comma separated list of navigation property paths
* @param {string} sEntitySetName the name of the entitySet the aDataSet belongs to
* @private
*/
MockServer.prototype._getOdataQueryExpand = function(aDataSet, sODataQueryValue, sEntitySetName) {
var that = this;
var aNavProperties = sODataQueryValue.split(',');
//trim all nav properties
jQuery.each(aNavProperties, function(i, sPropertyName) {
aNavProperties[i] = that._trim(sPropertyName);
});
var oEntitySetNavProps = that._mEntitySets[sEntitySetName].navprops;
jQuery.each(aDataSet, function(iIndex, oRecord) {
jQuery.each(aNavProperties, function(iIndex, sNavPropFull) {
var aNavProps = sNavPropFull.split("/");
var sNavProp = aNavProps[0];
if (!oRecord[sNavProp]) {
that._logAndThrowMockServerCustomError(404, that._oErrorMessages.RESOURCE_NOT_FOUND_FOR_SEGMENT, sNavProp);
}
//check if an expanded operation was already executed. for 1:* check results . otherwise, check if there is __deferred for clean start.
var aNavEntry = oRecord[sNavProp].results || oRecord[sNavProp];
if (!aNavEntry || !!aNavEntry.__deferred) {
aNavEntry = jQuery.extend(true, [], that._resolveNavigation(sEntitySetName, oRecord, sNavProp, oRecord));
} else if (!Array.isArray(aNavEntry)) {
aNavEntry = [aNavEntry];
}
if (!!aNavEntry && aNavProps.length > 1) {
var sRestNavProps = aNavProps.splice(1, aNavProps.length).join("/");
aNavEntry = that._getOdataQueryExpand(aNavEntry, sRestNavProps,
oEntitySetNavProps[sNavProp].to.entitySet);
}
if (oEntitySetNavProps[sNavProp].to.multiplicity === "*") {
oRecord[sNavProp] = {
results: aNavEntry
};
} else {
oRecord[sNavProp] = aNavEntry[0] ? aNavEntry[0] : {};
}
});
});
return aDataSet;
};
/**
* Refreshes the service metadata document and the mockdata
*
* @private
*/
MockServer.prototype._refreshData = function() {
// load the metadata
var oMetadata = this._loadMetadata(this._sMetadataString);
if (!oMetadata) {
return;
}
// here we need to analyse the EDMX and identify the entity sets
this._mEntitySets = this._findEntitySets(this._oMetadata);
this._mEntityTypes = this._findEntityTypes(this._oMetadata);
if (!this._sMockdataBaseUrl) {
// load the mockdata
this._generateMockdata(this._mEntitySets, this._oMetadata);
} else {
// check the mockdata base URL to end with a slash
if (!this._sMockdataBaseUrl.endsWith("/") && !this._sMockdataBaseUrl.endsWith(".json")) {
this._sMockdataBaseUrl += "/";
}
// load the mockdata
this._loadMockdata(this._mEntitySets, this._sMockdataBaseUrl);
}
};
/**
* Returns the root URI without query or hash parameters
* @return {string} the root URI without query or hash parameters
*/
MockServer.prototype._getRootUri = function() {
var sUri = this.getRootUri();
sUri = sUri && /([^?#]*)([?#].*)?/.exec(sUri)[1]; // remove URL parameters or anchors
return sUri;
};
/**
* Loads the service metadata for the given url
* @param {string} sMetadataUrl url to the service metadata document
* @return {XMLDocument} the xml document object
* @private
*/
MockServer.prototype._loadMetadata = function(sMetadata) {
var sMetadata;
sMetadata = sMetadata.trim();
// "<" as first character is a strong indicator for an XML-containing string. Everything else: URL...
if (sMetadata.substring(0,1) !== "<") {
// load the metadata as string to avoid usage of serializer
sMetadata = jQuery.sap.sjax({
url: sMetadata,
dataType: "text"
}).data;
if (!sMetadata) {
Log.error("MockServer: The metadata for url \"" + sMetadata + "\" could not be found!");
}
}
this._sMetadata = sMetadata;
try {
this._oMetadata = jQuery.parseXML(sMetadata);
} catch (oError) {
Log.error("MockServer: Invalid metadata XML! Reason: " + oError);
}
return this._oMetadata;
};
/**
* find the entity sets in the metadata XML document
* @param {XMLDocument} oMetadata the metadata XML document
* @return {map} map of entity sets
* @private
*/
MockServer.prototype._findEntitySets = function(oMetadata) {
// here we need to analyse the EDMX and identify the entity sets
var mEntitySets = {};
var oPrincipals = jQuery(oMetadata).find("Principal");
var oDependents = jQuery(oMetadata).find("Dependent");
jQuery(oMetadata).find("EntitySet").each(function(iIndex, oEntitySet) {
var $EntitySet = jQuery(oEntitySet);
// split the namespace and the name of the entity type (namespace could have dots inside)
var aEntityTypeParts = /((.*)\.)?(.*)/.exec($EntitySet.attr("EntityType"));
mEntitySets[$EntitySet.attr("Name")] = {
"name": $EntitySet.attr("Name"),
"schema": aEntityTypeParts[2],
"type": aEntityTypeParts[3],
"keys": [],
"keysType": {},
"navprops": {}
};
});
// helper function to find the entity set and property reference
// for the given role name
var fnResolveNavProp = function(sRole, aAssociation, aAssociationSet, bFrom) {
var sEntitySet = jQuery(aAssociationSet).find("End[Role='" + sRole + "']").attr("EntitySet");
var sMultiplicity = jQuery(aAssociation).find("End[Role='" + sRole + "']").attr("Multiplicity");
var aPropRef = [];
var aConstraint = jQuery(aAssociation).find("ReferentialConstraint > [Role='" + sRole + "']");
if (aConstraint && aConstraint.length > 0) {
jQuery(aConstraint[0]).children("PropertyRef").each(function(iIndex, oPropRef) {
aPropRef.push(jQuery(oPropRef).attr("Name"));
});
} else {
var oPrinDeps = (bFrom) ? oPrincipals : oDependents;
jQuery(oPrinDeps).each(function(iIndex, oPrinDep) {
if (sRole === (jQuery(oPrinDep).attr("Role"))) {
jQuery(oPrinDep).children("PropertyRef").each(function(iIndex, oPropRef) {
aPropRef.push(jQuery(oPropRef).attr("Name"));
});
return false;
}
});
}
return {
"role": sRole,
"entitySet": sEntitySet,
"propRef": aPropRef