@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,356 lines (1,233 loc) • 139 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/*eslint-disable max-len */
/**
* OData-based DataBinding
*
* @namespace
* @name sap.ui.model.odata
* @public
*/
// Provides class sap.ui.model.odata.ODataModel
sap.ui.define([
"./CountMode",
"./ODataContextBinding",
"./ODataListBinding",
"./ODataMetadata",
"./ODataPropertyBinding",
"./ODataTreeBinding",
"./ODataUtils",
"sap/base/assert",
"sap/base/Log",
"sap/base/security/encodeURL",
"sap/base/util/each",
"sap/base/util/extend",
"sap/base/util/isEmptyObject",
"sap/base/util/isPlainObject",
"sap/base/util/merge",
"sap/base/util/uid",
"sap/ui/core/Configuration",
"sap/ui/model/BindingMode",
"sap/ui/model/Context",
"sap/ui/model/FilterProcessor",
"sap/ui/model/Model",
"sap/ui/model/odata/ODataAnnotations",
"sap/ui/model/odata/ODataMetaModel",
"sap/ui/thirdparty/datajs",
"sap/ui/thirdparty/URI"
], function(CountMode, ODataContextBinding, ODataListBinding, ODataMetadata, ODataPropertyBinding,
ODataTreeBinding, ODataUtils, assert, Log, encodeURL, each, extend, isEmptyObject,
isPlainObject, merge, uid, Configuration, BindingMode, Context, FilterProcessor, Model,
ODataAnnotations, ODataMetaModel, OData, URI) {
"use strict";
/**
* Constructor for a new ODataModel.
*
* @param {string} [sServiceUrl] base uri of the service to request data from; additional URL parameters appended here will be appended to every request
* can be passed with the mParameters object as well: [mParameters.serviceUrl] A serviceURl is required!
* @param {object} [mParameters] (optional) a map which contains the following parameter properties:
* @param {boolean} [mParameters.json] if set true request payloads will be JSON, XML for false (default = false),
* @param {string} [mParameters.user] <b>Deprecated</b> for security reasons. Use strong server
* side authentication instead. UserID for the service.
* @param {string} [mParameters.password] <b>Deprecated</b> for security reasons. Use strong
* server side authentication instead. Password for the service.
* @param {Object<string,string>} [mParameters.headers] a map of custom headers like {"myHeader":"myHeaderValue",...},
* @param {boolean} [mParameters.tokenHandling] enable/disable XCSRF-Token handling (default = true),
* @param {boolean} [mParameters.withCredentials] experimental - true when user credentials are to be included in a cross-origin request. Please note that this works only if all requests are asynchronous.
* @param {object} [mParameters.loadMetadataAsync] (optional) determined if the service metadata request is sent synchronous or asynchronous. Default is false.
* @param [mParameters.maxDataServiceVersion] (default = '2.0') please use the following string format e.g. '2.0' or '3.0'.
* OData version supported by the ODataModel: '2.0',
* @param {boolean} [mParameters.useBatch] when true all requests will be sent in batch requests (default = false),
* @param {boolean} [mParameters.refreshAfterChange] enable/disable automatic refresh after change operations: default = true,
* @param {string|string[]} [mParameters.annotationURI] The URL (or an array of URLs) from which the annotation metadata should be loaded,
* @param {boolean} [mParameters.loadAnnotationsJoined] Whether or not to fire the metadataLoaded-event only after annotations have been loaded as well,
* @param {Object<string,string>} [mParameters.serviceUrlParams] map of URL parameters - these parameters will be attached to all requests,
* @param {Object<string,string>} [mParameters.metadataUrlParams] map of URL parameters for metadata requests - only attached to $metadata request.
* @param {string} [mParameters.defaultCountMode] sets the default count mode for the model. If not set, sap.ui.model.odata.CountMode.Both is used.
* @param {Object<string,string>} [mParameters.metadataNamespaces] a map of namespaces (name => URI) used for parsing the service metadata.
* @param {boolean} [mParameters.skipMetadataAnnotationParsing] Whether to skip the automated loading of annotations from the metadata document. Loading annotations from metadata does not have any effects (except the lost performance by invoking the parser) if there are not annotations inside the metadata document
*
* @class
* Model implementation for OData format
*
*
* @author SAP SE
* @version 1.111.5
*
* @public
* @deprecated As of version 1.48, please use {@link sap.ui.model.odata.v2.ODataModel} instead.
* @alias sap.ui.model.odata.ODataModel
* @extends sap.ui.model.Model
*/
var ODataModel = Model.extend("sap.ui.model.odata.ODataModel", /** @lends sap.ui.model.odata.ODataModel.prototype */ {
constructor : function(sServiceUrl, bJSON, sUser, sPassword, mHeaders, bTokenHandling, bWithCredentials, bLoadMetadataAsync) {
Model.apply(this, arguments);
var bUseBatch,
bRefreshAfterChange,
sMaxDataServiceVersion,
sAnnotationURI = null,
bLoadAnnotationsJoined,
mMetadataNamespaces,
sDefaultCountMode,
mServiceUrlParams,
mMetadataUrlParams,
bSkipMetadataAnnotationParsing,
that = this;
if (typeof (sServiceUrl) === "object") {
bJSON = sServiceUrl;
sServiceUrl = bJSON.serviceUrl;
}
if (typeof bJSON === "object") {
sUser = bJSON.user;
sPassword = bJSON.password;
mHeaders = bJSON.headers;
bTokenHandling = bJSON.tokenHandling;
bLoadMetadataAsync = bJSON.loadMetadataAsync;
bWithCredentials = bJSON.withCredentials;
sMaxDataServiceVersion = bJSON.maxDataServiceVersion;
bUseBatch = bJSON.useBatch;
bRefreshAfterChange = bJSON.refreshAfterChange;
sAnnotationURI = bJSON.annotationURI;
bLoadAnnotationsJoined = bJSON.loadAnnotationsJoined;
sDefaultCountMode = bJSON.defaultCountMode;
mMetadataNamespaces = bJSON.metadataNamespaces;
mServiceUrlParams = bJSON.serviceUrlParams;
mMetadataUrlParams = bJSON.metadataUrlParams;
bSkipMetadataAnnotationParsing = bJSON.skipMetadataAnnotationParsing;
bJSON = bJSON.json;
}
this.oServiceData = {};
this.sDefaultBindingMode = BindingMode.OneWay;
this.mSupportedBindingModes = {"OneWay": true, "OneTime": true, "TwoWay":true};
this.mUnsupportedFilterOperators = {"Any": true, "All": true};
this.bCountSupported = true;
this.bJSON = bJSON;
this.bCache = true;
this.aPendingRequestHandles = [];
this.oRequestQueue = {};
this.aBatchOperations = [];
this.oHandler = undefined;
this.bTokenHandling = bTokenHandling !== false;
this.bWithCredentials = bWithCredentials === true;
this.bUseBatch = bUseBatch === true;
this.bRefreshAfterChange = bRefreshAfterChange !== false;
this.sMaxDataServiceVersion = sMaxDataServiceVersion;
this.bLoadMetadataAsync = !!bLoadMetadataAsync;
this.bLoadAnnotationsJoined = bLoadAnnotationsJoined === undefined ? true : bLoadAnnotationsJoined;
this.sAnnotationURI = sAnnotationURI;
this.sDefaultCountMode = sDefaultCountMode || CountMode.Both;
this.oMetadataLoadEvent = null;
this.oMetadataFailedEvent = null;
this.bSkipMetadataAnnotationParsing = bSkipMetadataAnnotationParsing;
// prepare variables for request headers, data and metadata
this.oHeaders = {};
this.setHeaders(mHeaders);
this.oData = {};
this.oMetadata = null;
this.oAnnotations = null;
this.aUrlParams = [];
// determine the service base url and the url parameters
if (sServiceUrl.indexOf("?") == -1) {
this.sServiceUrl = sServiceUrl;
} else {
var aUrlParts = sServiceUrl.split("?");
this.sServiceUrl = aUrlParts[0];
if (aUrlParts[1]) {
this.aUrlParams.push(aUrlParts[1]);
}
}
if (Configuration.getStatisticsEnabled()) {
// add statistics parameter to every request (supported only on Gateway servers)
this.aUrlParams.push("sap-statistics=true");
}
// Remove trailing slash (if any)
this.sServiceUrl = this.sServiceUrl.replace(/\/$/, "");
// Get/create service specific data container or create one if it doesn't exist
var sMetadataUrl = this._createRequestUrl("$metadata", undefined, mMetadataUrlParams);
if (!ODataModel.mServiceData[sMetadataUrl]) {
ODataModel.mServiceData[sMetadataUrl] = {};
}
this.oServiceData = ODataModel.mServiceData[sMetadataUrl];
// Get CSRF token, if already available
if (this.bTokenHandling && this.oServiceData.securityToken) {
this.oHeaders["x-csrf-token"] = this.oServiceData.securityToken;
}
// store user and password
this.sUser = sUser;
this.sPassword = sPassword;
this.oHeaders["Accept-Language"] = Configuration.getLanguageTag();
if (!this.oServiceData.oMetadata) {
//create Metadata object
this.oServiceData.oMetadata = new ODataMetadata(sMetadataUrl, {
async: this.bLoadMetadataAsync,
user: this.sUser,
password: this.sPassword,
headers: this.mCustomHeaders,
namespaces: mMetadataNamespaces,
withCredentials: this.bWithCredentials
});
}
this.oMetadata = this.oServiceData.oMetadata;
this.pAnnotationsLoaded = this.oMetadata.loaded();
if (this.sAnnotationURI || !this.bSkipMetadataAnnotationParsing) {
// Make sure the annotation parser object is already created and can be used by the MetaModel
var oAnnotations = this._getAnnotationParser();
if (!this.bSkipMetadataAnnotationParsing) {
if (!this.bLoadMetadataAsync) {
//Synchronous metadata load --> metadata should already be available, try to stay synchronous
// Don't fire additional events for automatic metadata annotations parsing, but if no annotation URL exists, fire the event
this.addAnnotationXML(this.oMetadata.sMetadataBody, !!this.sAnnotationURI);
} else {
this.pAnnotationsLoaded = this.oMetadata.loaded().then(function(bSuppressEvents, mParams) {
//Don't fire additional events for automatic metadata annotations parsing, but if no annotation URL exists, fire the event
if (this.bDestroyed) {
return Promise.reject();
}
return this.addAnnotationXML(mParams["metadataString"], bSuppressEvents);
}.bind(this, !!this.sAnnotationURI));
}
}
if (this.sAnnotationURI) {
if (this.bLoadMetadataAsync) {
this.pAnnotationsLoaded = this.pAnnotationsLoaded
.then(oAnnotations.addUrl.bind(oAnnotations, this.sAnnotationURI));
} else {
this.pAnnotationsLoaded = Promise.all([
this.pAnnotationsLoaded,
oAnnotations.addUrl(this.sAnnotationURI)
]);
}
}
}
if (mServiceUrlParams) {
// new URL params used -> add to ones from sServiceUrl
// do this after the Metadata request is created to not put the serviceUrlParams on this one
this.aUrlParams = this.aUrlParams.concat(ODataUtils._createUrlParamsArray(mServiceUrlParams));
}
this.onMetadataLoaded = function(oEvent){
that._initializeMetadata();
that.initialize();
};
this.onMetadataFailed = function(oEvent) {
that.fireMetadataFailed(oEvent.getParameters());
};
if (!this.oMetadata.isLoaded()) {
this.oMetadata.attachLoaded(this.onMetadataLoaded);
this.oMetadata.attachFailed(this.onMetadataFailed);
}
if (this.oMetadata.isFailed()){
this.refreshMetadata();
}
if (this.oMetadata.isLoaded()) {
this._initializeMetadata(true);
}
// set the header for the accepted content types
if (this.bJSON) {
if (this.sMaxDataServiceVersion === "3.0") {
this.oHeaders["Accept"] = "application/json;odata=fullmetadata";
} else {
this.oHeaders["Accept"] = "application/json";
}
this.oHandler = OData.jsonHandler;
} else {
this.oHeaders["Accept"] = "application/atom+xml,application/atomsvc+xml,application/xml";
this.oHandler = OData.atomHandler;
}
// the max version number the client can accept in a response
this.oHeaders["MaxDataServiceVersion"] = "2.0";
if (this.sMaxDataServiceVersion) {
this.oHeaders["MaxDataServiceVersion"] = this.sMaxDataServiceVersion;
}
// set version to 2.0 because 1.0 does not support e.g. skip/top, inlinecount...
// states the version of the Open Data Protocol used by the client to generate the request.
this.oHeaders["DataServiceVersion"] = "2.0";
},
metadata : {
publicMethods : ["create", "remove", "update", "submitChanges", "getServiceMetadata", "read", "hasPendingChanges", "refresh", "refreshMetadata", "resetChanges",
"isCountSupported", "setCountSupported", "setDefaultCountMode", "getDefaultCountMode", "forceNoCache", "setProperty",
"getSecurityToken", "refreshSecurityToken", "setHeaders", "getHeaders", "setUseBatch"]
}
});
//
ODataModel.M_EVENTS = {
RejectChange: "rejectChange",
/**
* Event is fired if the metadata document was successfully loaded
*/
MetadataLoaded: "metadataLoaded",
/**
* Event is fired if the metadata document has failed to load
*/
MetadataFailed: "metadataFailed",
/**
* Event is fired if the annotations document was successfully loaded
*/
AnnotationsLoaded: "annotationsLoaded",
/**
* Event is fired if the annotations document has failed to load
*/
AnnotationsFailed: "annotationsFailed"
};
// Keep a map of service specific data, which can be shared across different model instances
// on the same OData service
ODataModel.mServiceData = {
};
ODataModel.prototype.fireRejectChange = function(mParameters) {
this.fireEvent("rejectChange", mParameters);
return this;
};
ODataModel.prototype.attachRejectChange = function(oData, fnFunction, oListener) {
this.attachEvent("rejectChange", oData, fnFunction, oListener);
return this;
};
ODataModel.prototype.detachRejectChange = function(fnFunction, oListener) {
this.detachEvent("rejectChange", fnFunction, oListener);
return this;
};
/**
* Fires the "metadataLoaded" event if the metadata and the annotations are loaded.
*
* @param {boolean} bDelayEvent
* Whether the <code>fireMetadataLoaded</code>-event should be fired with a delay
* @private
*/
ODataModel.prototype._initializeMetadata = function(bDelayEvent) {
var that = this;
this.bUseBatch = this.bUseBatch || this.oMetadata.getUseBatch();
var doFire = function(bDelay){
if (bDelay) {
that.metadataLoadEvent = setTimeout(doFire.bind(that), 0);
} else if (that.oMetadata) {
that.fireMetadataLoaded({metadata: that.oMetadata});
Log.debug("ODataModel fired metadataloaded");
}
};
if (this.sAnnotationURI && this.bLoadAnnotationsJoined) {
// In case of joined loading, wait for the annotations before firing the event
// This is also tested in the fireMetadataLoaded-method and no event is fired in case
// of joined loading.
if (this.oAnnotations && (this.oAnnotations.bInitialized || this.oAnnotations.isFailed())) {
doFire(!this.bLoadMetadataAsync);
} else {
this.oAnnotations.attachEventOnce("loaded", function() {
doFire(true);
});
}
} else {
// In case of synchronous or asynchronous non-joined loading, or if no annotations are
// loaded at all, the events are fired individually
doFire(bDelayEvent);
}
};
/**
* The <code>annotationsLoaded</code> event is fired after the annotations document was successfully loaded.
*
* @name sap.ui.model.odata.ODataModel#annotationsLoaded
* @event
* @param {sap.ui.base.Event} oEvent
* @public
*/
/**
* Fires event {@link #event:annotationsLoaded annotationsLoaded} to attached listeners.
*
* @param {object} [oParameters] Parameters to pass along with the event
* @param {sap.ui.model.odata.ODataAnnotations} [oParameters.annotations] the annotations object.
*
* @return {this} <code>this</code> to allow method chaining
* @protected
*/
ODataModel.prototype.fireAnnotationsLoaded = function(oParameters) {
if (!this.bLoadMetadataAsync) {
setTimeout(this.fireEvent.bind(this, "annotationsLoaded", oParameters), 0);
} else {
this.fireEvent("annotationsLoaded", oParameters);
}
return this;
};
/**
* Attaches event handler <code>fnFunction</code> to the {@link #event:annotationsLoaded annotationsLoaded} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* @param {object}
* [oData] An application-specific payload object that will be passed to the event handler
* along with the event object when firing the event
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object to call the event handler with. Defaults to this
* <code>sap.ui.model.odata.ODataModel</code> itself
*
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.attachAnnotationsLoaded = function(oData, fnFunction, oListener) {
this.attachEvent("annotationsLoaded", oData, fnFunction, oListener);
return this;
};
/**
* Detaches event handler <code>fnFunction</code> from the {@link #event:annotationsLoaded annotationsLoaded} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object on which the given function had to be called
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.detachAnnotationsLoaded = function(fnFunction, oListener) {
this.detachEvent("annotationsLoaded", fnFunction, oListener);
return this;
};
/**
* The <code>annotationsFailed</code> event is fired when loading the annotations document failed.
*
* @name sap.ui.model.odata.ODataModel#annotationsFailed
* @event
* @param {sap.ui.base.Event} oEvent
* @public
*/
/**
* Fires event {@link #event:annotationsFailed annotationsFailed} to attached listeners.
*
* @param {object} [oParameters] Parameters to pass along with the event
* @param {string} [oParameters.message] A text that describes the failure.
* @param {string} [oParameters.statusCode] HTTP status code returned by the request (if available)
* @param {string} [oParameters.statusText] The status as a text, details not specified, intended only for diagnosis output
* @param {string} [oParameters.responseText] Response that has been received for the request ,as a text string
*
* @return {this} <code>this</code> to allow method chaining
* @protected
*/
ODataModel.prototype.fireAnnotationsFailed = function(oParameters) {
if (!this.bLoadMetadataAsync) {
setTimeout(this.fireEvent.bind(this, "annotationsFailed", oParameters), 0);
} else {
this.fireEvent("annotationsFailed", oParameters);
}
Log.debug("ODataModel fired annotationsFailed");
return this;
};
/**
* Attaches event handler <code>fnFunction</code> to the {@link #event:annotationsFailed annotationsFailed} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* @param {object}
* [oData] An application-specific payload object that will be passed to the event handler
* along with the event object when firing the event
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object to call the event handler with. Defaults to this
* <code>sap.ui.model.odata.ODataModel</code> itself
*
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.attachAnnotationsFailed = function(oData, fnFunction, oListener) {
this.attachEvent("annotationsFailed", oData, fnFunction, oListener);
return this;
};
/**
* Detaches event handler <code>fnFunction</code> from the {@link #event:annotationsFailed annotationsFailed} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* The passed function and listener object must match the ones used for event registration.
*
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object on which the given function had to be called
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.detachAnnotationsFailed = function(fnFunction, oListener) {
this.detachEvent("annotationsFailed", fnFunction, oListener);
return this;
};
/**
* The <code>metadataLoaded</code> event is fired after the metadata document was successfully loaded.
*
* @name sap.ui.model.odata.ODataModel#metadataLoaded
* @event
* @param {sap.ui.base.Event} oEvent
* @public
*/
/**
* Fires event {@link #event:metadataLoaded metadataLoaded} to attached listeners.
*
* @param {object} [oParameters] Parameters to pass along with the event
* @param {sap.ui.model.odata.ODataMetadata} [oParameters.metadata] The metadata object.
*
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @protected
*/
ODataModel.prototype.fireMetadataLoaded = function(oParameters) {
this.fireEvent("metadataLoaded", oParameters);
return this;
};
/**
* Attaches event handler <code>fnFunction</code> to the {@link #event:metadataLoaded metadataLoaded} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* @param {object}
* [oData] An application-specific payload object that will be passed to the event handler
* along with the event object when firing the event
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object to call the event handler with. Defaults to this
* <code>sap.ui.model.odata.ODataModel</code> itself
*
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.attachMetadataLoaded = function(oData, fnFunction, oListener) {
this.attachEvent("metadataLoaded", oData, fnFunction, oListener);
return this;
};
/**
* Detaches event handler <code>fnFunction</code> from the {@link #event:metadataLoaded metadataLoaded} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* The passed function and listener object must match the ones used for event registration.
*
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object on which the given function had to be called
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.detachMetadataLoaded = function(fnFunction, oListener) {
this.detachEvent("metadataLoaded", fnFunction, oListener);
return this;
};
/**
* The <code>metadataFailed</code> event is fired when loading the metadata document failed.
*
* @name sap.ui.model.odata.ODataModel#metadataFailed
* @event
* @param {sap.ui.base.Event} oEvent
* @public
*/
/**
* Fires event {@link #event:metadataFailed metadataFailed} to attached listeners.
*
* @param {object} [oParameters] Parameters to pass along with the event
* @param {string} [oParameters.message] A text that describes the failure.
* @param {string} [oParameters.statusCode] HTTP status code returned by the request (if available)
* @param {string} [oParameters.statusText] The status as a text, details not specified, intended only for diagnosis output
* @param {string} [oParameters.responseText] Response that has been received for the request ,as a text string
*
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @protected
*/
ODataModel.prototype.fireMetadataFailed = function(oParameters) {
this.fireEvent("metadataFailed", oParameters);
return this;
};
/**
* Attaches event handler <code>fnFunction</code> to the {@link #event:metadataFailed metadataFailed} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* @param {object}
* [oData] An application-specific payload object that will be passed to the event handler
* along with the event object when firing the event
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object to call the event handler with. Defaults to this
* <code>sap.ui.model.odata.ODataModel</code> itself
*
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.attachMetadataFailed = function(oData, fnFunction, oListener) {
this.attachEvent("metadataFailed", oData, fnFunction, oListener);
return this;
};
/**
* Detaches event handler <code>fnFunction</code> from the {@link #event:metadataFailed metadataFailed} event of this
* <code>sap.ui.model.odata.ODataModel</code>.
*
* The passed function and listener object must match the ones used for event registration.
*
* @param {function}
* fnFunction The function to be called, when the event occurs
* @param {object}
* [oListener] Context object on which the given function had to be called
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ODataModel.prototype.detachMetadataFailed = function(fnFunction, oListener) {
this.detachEvent("metadataFailed", fnFunction, oListener);
return this;
};
/**
* Refreshes the metadata for model, e.g. in case the first request for metadata has failed.
*
* @public
*/
ODataModel.prototype.refreshMetadata = function(){
if (this.oMetadata && this.oMetadata.refresh){
this.oMetadata.refresh();
}
};
/**
* Creates a request URL using the supplied parameters.
*
* @param {string} sPath The path to the property
* @param {sap.ui.model.Context} oContext The context of the property
* @param {object} oUrlParams Additional query parameters
* @param {boolean} bBatch Whether a batch request should be sent
* @param {boolean} [bCache=true] Force no caching if false
*
* @returns {string} The created URL
* @private
*/
ODataModel.prototype._createRequestUrl = function(sPath, oContext, oUrlParams, bBatch, bCache) {
// create the url for the service
var aUrlParams,
sResolvedPath,
sUrlParams,
sUrl = "";
//we need to handle url params that can be passed from the manual CRUD methods due to compatibility
if (sPath && sPath.indexOf('?') != -1 ) {
sUrlParams = sPath.substr(sPath.indexOf('?') + 1);
sPath = sPath.substr(0, sPath.indexOf('?'));
}
sResolvedPath = this._normalizePath(sPath, oContext);
if (!bBatch) {
sUrl = this.sServiceUrl + sResolvedPath;
} else {
sUrl = sResolvedPath.substr(sResolvedPath.indexOf('/') + 1);
}
aUrlParams = ODataUtils._createUrlParamsArray(oUrlParams);
if (this.aUrlParams) {
aUrlParams = aUrlParams.concat(this.aUrlParams);
}
if (sUrlParams) {
aUrlParams.push(sUrlParams);
}
if (aUrlParams.length > 0) {
sUrl += "?" + aUrlParams.join("&");
}
if (bCache === undefined) {
bCache = true;
}
if (bCache === false) {
var timeStamp = Date.now();
// try replacing _= if it is there
var ret = sUrl.replace( /([?&])_=[^&]*/, "$1_=" + timeStamp );
// if nothing was replaced, add timestamp to the end
sUrl = ret + ( ( ret === sUrl ) ? ( /\?/.test( sUrl ) ? "&" : "?" ) + "_=" + timeStamp : "" );
}
return sUrl;
};
/**
* Does a request using the service URL and configuration parameters provided in the model's
* constructor and sets the response data into the model. This request is performed
* asynchronously.
*
* @param {string} sPath
* A string containing the path to the data which should be retrieved; the path is appended
* to the <code>sServiceUrl</code> which was specified in the model constructor
* @param {string[]} aParams
* Additional query parameters
* @param {function} [fnSuccess]
* Callback function which is called when the data has been successfully retrieved and stored
* in the model
* @param {function} [fnError]
* Callback function which is called when the request failed
* @param {boolean} [bCache=true]
* Force no caching if false
* @param {function} [fnHandleUpdate]
* Function to handle an update with
* @param {function} [fnCompleted]
* Function to call after the request is completed; called after <code>fnSuccess</code>
*
* @private
*/
ODataModel.prototype._loadData = function(sPath, aParams, fnSuccess, fnError, bCache,
fnHandleUpdate, fnCompleted){
// create a request object for the data request
var oRequestHandle,
aResults = [],
sUrl = this._createRequestUrl(sPath, null, aParams, null, bCache || this.bCache),
oRequest = this._createRequest(sUrl, "GET", true),
that = this;
function _handleSuccess(oData, oResponse) {
var oResultData = oData,
mChangedEntities = {};
// no data response
if (oResponse.statusCode == 204) {
if (fnSuccess) {
fnSuccess(null);
}
if (fnCompleted) {
fnCompleted(null);
}
that.fireRequestCompleted({url : oRequest.requestUri, type : "GET", async : oRequest.async,
info: "Accept headers:" + that.oHeaders["Accept"], infoObject : {acceptHeaders: that.oHeaders["Accept"]}, success: true});
return undefined;
}
// no data available
if (!oResultData) {
Log.fatal("The following problem occurred: No data was retrieved by service: " + oResponse.requestUri);
that.fireRequestCompleted({url : oRequest.requestUri, type : "GET", async : oRequest.async,
info: "Accept headers:" + that.oHeaders["Accept"], infoObject : {acceptHeaders: that.oHeaders["Accept"]}, success: false});
return false;
}
if (that.bUseBatch) { // process batch response
// check if errors occurred in the batch
var aErrorResponses = that._getBatchErrors(oData);
if (aErrorResponses.length > 0) {
// call handle error with the first error.
_handleError(aErrorResponses[0]);
return false;
}
if (oResultData.__batchResponses && oResultData.__batchResponses.length > 0) {
oResultData = oResultData.__batchResponses[0].data;
} else {
Log.fatal("The following problem occurred: No data was retrieved by service: " + oResponse.requestUri);
}
}
aResults = aResults.concat(oResultData.results);
// check if not all requested data was loaded
if (oResultData.__next) {
// replace request uri with next uri to retrieve additional data
var oURI = new URI(oResultData.__next);
oRequest.requestUri = oURI.absoluteTo(oResponse.requestUri).toString();
_submit(oRequest);
} else {
// all data is read so merge all data
if (oResultData.results) {
var vValue, vKey;
for (vKey in aResults) {
vValue = aResults[vKey];
// Prevent never-ending loop
if (aResults === vValue) {
continue;
}
oResultData.results[vKey] = vValue;
}
}
// broken implementations need this
if (oResultData.results && !Array.isArray(oResultData.results)) {
oResultData = oResultData.results;
}
// adding the result data to the data object
that._importData(oResultData, mChangedEntities);
// reset change key if refresh was triggered on that entry
if (that.sChangeKey && mChangedEntities) {
var sEntry = that.sChangeKey.substr(that.sChangeKey.lastIndexOf('/') + 1);
if (mChangedEntities[sEntry]) {
delete that.oRequestQueue[that.sChangeKey];
that.sChangeKey = null;
}
}
if (fnSuccess) {
fnSuccess(oResultData);
}
that.checkUpdate(false, false, mChangedEntities);
if (fnCompleted) {
fnCompleted(oResultData);
}
that.fireRequestCompleted({url : oRequest.requestUri, type : "GET", async : oRequest.async,
info: "Accept headers:" + that.oHeaders["Accept"], infoObject : {acceptHeaders: that.oHeaders["Accept"]}, success: true});
}
return undefined;
}
function _handleError(oError) {
// If error is a 403 with XSRF token "Required" reset token and retry sending request
if (that.bTokenHandling && oError.response) {
var sToken = that._getHeader("x-csrf-token", oError.response.headers);
if (!oRequest.bTokenReset && oError.response.statusCode == '403' && sToken && sToken.toLowerCase() == "required") {
that.resetSecurityToken();
oRequest.bTokenReset = true;
_submit();
return;
}
}
var mParameters = that._handleError(oError);
if (fnError) {
fnError(oError, oRequestHandle && oRequestHandle.bAborted);
}
that.fireRequestCompleted({url : oRequest.requestUri, type : "GET", async : oRequest.async,
info: "Accept headers:" + that.oHeaders["Accept"], infoObject : {acceptHeaders: that.oHeaders["Accept"]}, success: false, errorobject: mParameters});
// Don't fire RequestFailed for intentionally aborted requests; fire event if we have no (OData.read fails before handle creation)
if (!oRequestHandle || !oRequestHandle.bAborted) {
mParameters.url = oRequest.requestUri;
that.fireRequestFailed(mParameters);
}
}
/**
* this method is used to retrieve all desired data. It triggers additional read requests if the server paging size
* permits to return all the requested data. This could only happen for servers with support for OData > 2.0.
*/
function _submit(){
// execute the request and use the metadata if available
if (that.bUseBatch) {
that.updateSecurityToken();
// batch requests only need the path without the service URL
// extract query of url and combine it with the path...
var sUriQuery = URI.parse(oRequest.requestUri).query;
//var sRequestUrl = sPath.replace(/\/$/, ""); // remove trailing slash if any
//sRequestUrl += sUriQuery ? "?" + sUriQuery : "";
var sRequestUrl = that._createRequestUrl(sPath, null, sUriQuery, that.bUseBatch);
oRequest = that._createRequest(sRequestUrl, "GET", true);
// Make sure requests not requiring a CSRF token don't send one.
if (that.bTokenHandling) {
delete oRequest.headers["x-csrf-token"];
}
var oBatchRequest = that._createBatchRequest([oRequest],true);
oRequestHandle = that._request(oBatchRequest, _handleSuccess, _handleError, OData.batchHandler, undefined, that.getServiceMetadata());
} else {
oRequestHandle = that._request(oRequest, _handleSuccess, _handleError, that.oHandler, undefined, that.getServiceMetadata());
}
if (fnHandleUpdate) {
// Create a wrapper for the request handle to be able to differentiate
// between intentionally aborted requests and failed requests
var oWrappedHandle = {
abort: function() {
oRequestHandle.bAborted = true;
oRequestHandle.abort();
}
};
fnHandleUpdate(oWrappedHandle);
}
}
// Make sure requests not requiring a CSRF token don't send one.
if (that.bTokenHandling) {
delete oRequest.headers["x-csrf-token"];
}
this.fireRequestSent({url : oRequest.requestUri, type : "GET", async : oRequest.async,
info: "Accept headers:" + this.oHeaders["Accept"], infoObject : {acceptHeaders: this.oHeaders["Accept"]}});
_submit();
};
/**
* Imports the data to the internal storage. Nested entries are processed recursively, moved to
* the canonic location and referenced from the parent entry. Keys are collected in a map for
* updating bindings.
*
* @param {object} oData
* The data
* @param {Object<string,boolean>} mKeys
* Keys used to update affected bindings
*
* @returns {string[]|string}
* Returns an array of keys if the data has nested data or a single key if it doesn't have
* nested data
*/
ODataModel.prototype._importData = function(oData, mKeys) {
var that = this,
aList, sKey, oResult, oEntry;
if (oData.results) {
aList = [];
each(oData.results, function(i, entry) {
aList.push(that._importData(entry, mKeys));
});
return aList;
} else {
sKey = this._getKey(oData);
oEntry = this.oData[sKey];
if (!oEntry) {
oEntry = oData;
this.oData[sKey] = oEntry;
}
each(oData, function(sName, oProperty) {
if (oProperty && (oProperty.__metadata && oProperty.__metadata.uri || oProperty.results) && !oProperty.__deferred) {
oResult = that._importData(oProperty, mKeys);
if (Array.isArray(oResult)) {
oEntry[sName] = { __list: oResult };
} else {
oEntry[sName] = { __ref: oResult };
}
} else if (!oProperty || !oProperty.__deferred) { //do not store deferred navprops
oEntry[sName] = oProperty;
}
});
mKeys[sKey] = true;
return sKey;
}
};
/**
* Remove references of navigation properties created in <code>importData</code> function.
*
* @param {object} oData Data imported in <code>importData</code>
*
* @returns {object|object[]} The data with references of navigation properties removed
*/
ODataModel.prototype._removeReferences = function(oData){
var that = this, aList;
if (oData.results) {
aList = [];
each(oData.results, function(i, entry) {
aList.push(that._removeReferences(entry));
});
return aList;
} else {
each(oData, function(sPropName, oCurrentEntry) {
if (oCurrentEntry) {
if (oCurrentEntry["__ref"] || oCurrentEntry["__list"]) {
delete oData[sPropName];
}
}
});
return oData;
}
};
/**
* Restore references of navigation properties created in <code>importData</code> function.
*
* @param {object} oData Data imported in <code>importData</code>
*
* @returns {object|object[]} The data with references of navigation properties restored
*/
ODataModel.prototype._restoreReferences = function(oData){
var that = this,
aList,
aResults = [];
if (oData.results) {
aList = [];
each(oData.results, function(i, entry) {
aList.push(that._restoreReferences(entry));
});
return aList;
} else {
each(oData, function(sPropName, oCurrentEntry) {
if (oCurrentEntry && oCurrentEntry["__ref"]) {
var oChildEntry = that._getObject("/" + oCurrentEntry["__ref"]);
assert(oChildEntry, "ODataModel inconsistent: " + oCurrentEntry["__ref"] + " not found!");
if (oChildEntry) {
delete oCurrentEntry["__ref"];
oData[sPropName] = oChildEntry;
// check recursively for found child entries
that._restoreReferences(oChildEntry);
}
} else if (oCurrentEntry && oCurrentEntry["__list"]) {
each(oCurrentEntry["__list"], function(j, sEntry) {
var oChildEntry = that._getObject("/" + oCurrentEntry["__list"][j]);
assert(oChildEntry, "ODataModel inconsistent: " + oCurrentEntry["__list"][j] + " not found!");
if (oChildEntry) {
aResults.push(oChildEntry);
// check recursively for found child entries
that._restoreReferences(oChildEntry);
}
});
delete oCurrentEntry["__list"];
oCurrentEntry.results = aResults;
aResults = [];
}
});
return oData;
}
};
/**
* removes all existing data from the model
*/
ODataModel.prototype.removeData = function(){
this.oData = {};
};
/**
* Initialize the model.
* This will call initialize on all bindings. This is done if metadata is loaded asynchronously.
*
* @private
*/
ODataModel.prototype.initialize = function() {
// Call initialize on all bindings in case metadata was not available when they were created
var aBindings = this.getBindings();
each(aBindings, function(iIndex, oBinding) {
oBinding.initialize();
});
};
/**
* Refresh the model.
*
* This will check all bindings for updated data and update the controls if data has been changed.
*
* @param {boolean} [bForceUpdate=false] Force update of controls
* @param {boolean} [bRemoveData=false] If set to true then the model data will be removed/cleared.
* Please note that the data might not be there when calling e.g. getProperty too early before the refresh call returned.
*
* @public
*/
ODataModel.prototype.refresh = function(bForceUpdate, bRemoveData) {
// Call refresh on all bindings instead of checkUpdate to properly reset cached data in bindings
if (bRemoveData) {
this.removeData();
}
this._refresh(bForceUpdate);
};
ODataModel.prototype._refresh = function(bForceUpdate, mChangedEntities, mEntityTypes) {
// Call refresh on all bindings instead of checkUpdate to properly reset cached data in bindings
var aBindings = this.getBindings();
each(aBindings, function(iIndex, oBinding) {
oBinding.refresh(bForceUpdate, mChangedEntities, mEntityTypes);
});
};
/**
* Private method iterating the registered bindings of this model instance and initiating their
* check for an update.
*
* @param {boolean} [bForceUpdate]
* Whether change events is fired regardless of the bindings state
* @param {boolean} bAsync
* Whether the check is done asynchronously
* @param {object} mChangedEntities
* A map of changed entities
* @param {boolean} bMetaModelOnly
* Whether only metamodel bindings are updated
*
* @private
*/
ODataModel.prototype.checkUpdate = function(bForceUpdate, bAsync, mChangedEntities, bMetaModelOnly) {
if (bAsync) {
if (!this.sUpdateTimer) {
this.sUpdateTimer = setTimeout(function() {
this.checkUpdate(bForceUpdate, false, mChangedEntities);
}.bind(this), 0);
}
return;
}
if (this.sUpdateTimer) {
clearTimeout(this.sUpdateTimer);
this.sUpdateTimer = null;
}
var aBindings = this.getBindings();
each(aBindings, function(iIndex, oBinding) {
if (!bMetaModelOnly || this.isMetaModelPath(oBinding.getPath())) {
oBinding.checkUpdate(bForceUpdate, mChangedEntities);
}
}.bind(this));
};
/*
* @see sap.ui.model.Model.prototype.bindProperty
*/
ODataModel.prototype.bindProperty = function(sPath, oContext, mParameters) {
var oBinding = new ODataPropertyBinding(this, sPath, oContext, mParameters);
return oBinding;
};
/**
* Creates a new list binding for this model.
*
* @param {string} sPath
* Binding path, either absolute or relative to a given <code>oContext</code>
* @param {sap.ui.model.Context} [oContext]
* Binding context referring to this model
* @param {sap.ui.model.Sorter|sap.ui.model.Sorter[]} [aSorters]
* Initial sort order, can be either a sorter or an array of sorters
* @param {sap.ui.model.Filter|sap.ui.model.Filter[]} [aFilters]
* Predefined filter/s, can be either a filter or an array of filters
* @param {object} [mParameters]
* Map which contains additional parameters for the binding
* @param {string} [mParameters.expand]
* Value for the OData <code>$expand</code> query parameter which should be included in the
* request
* @param {string} [mParameters.select]
* Value for the OData <code>$select</code> query parameter which should be included in the
* request
* @param {Object<string,string>} [mParameters.custom]
* Optional map of custom query parameters (name/value pairs); names of custom parameters must
* not start with <code>$</code>
* @param {sap.ui.model.odata.CountMode} [mParameters.countMode]
* Defines the count mode of the new binding; if not specified, the default count mode of this
* model will be applied
*
* @returns {sap.ui.model.ListBinding} A new list binding object
*
* @see sap.ui.model.Model.prototype.bindList
* @public
*/
ODataModel.prototype.bindList = function(sPath, oContext, aSorters, aFilters, mParameters) {
var oBinding = new ODataListBinding(this, sPath, oContext, aSorters, aFilters, mParameters);
return oBinding;
};
/*
* @see sap.ui.model.Model.prototype.bindTree
*/
ODataModel.prototype.bindTree = function(sPath, oContext, aFilters, mParameters) {
var oBinding = new ODataTreeBinding(this, sPath, oContext, aFilters, mParameters);
return oBinding;
};
/*
* Creates a binding context for the given path
* If the data of the context is not yet available, it can not be created, but first the
* entity needs to be fetched from the server asynchronously. In case no callback function
* is provided, the request will not be triggered.
*
* @see sap.ui.model.Model.prototype.createBindingContext
*/
ODataModel.prototype.createBindingContext =
function(sPath, oContext, mParameters, fnCallBack, bReload) {
var sFullPath = this.resolve(sPath, oContext);
bReload = !!bReload;
// optional parameter handling
if (typeof oContext == "function") {
fnCallBack = oContext;
oContext = null;
}
if (typeof mParameters == "function") {
fnCallBack = mParameters;
mParameters = null;
}
// if path cannot be resolved, call the callback function and return null
if (!sFullPath) {
if (fnCallBack) {
fnCallBack(null);
}
return null;
}
// try to resolve path, send a request to the server if data is not available yet
// if we have set forceUpdate in mParameters we send the request even if the data is available
var oData = this._getObject(sPath, oContext),
sKey,
oNewContext,
that = this;
if (!bReload) {
bReload = this._isReloadNeeded(sFullPath, oData, mParameters);
}
if (!bReload) {
sKey = this._getKey(oData);
oNewContext = this.getContext('/' + sKey);
if (fnCallBack) {
fnCallBack(oNewContext);
}
return oNewContext;
}
if (fnCallBack) {
var bIsRelative = !sPath.startsWith("/");
if (sFullPath) {
var aParams = [],
sCustomParams = this.createCustomParams(mParameters);
if (sCustomParams) {
aParams.push(sCustomParams);
}
this._loadData(sFullPath, aParams, function(oData) {
sKey = oData ? that._getKey(oData) : undefined;
if (sKey && oContext && bIsRelative) {
var sContextPath = oContext.getPath();
// remove starting slash
sContextPath = sContextPath.substr(1);
// when model is refreshed, parent entity might not be available yet
if (that.oData[sContextPath]) {
that.oData[sContextPath][sPath] = {__ref: sKey};
}
}
oNewContext = that.getContext('/' + sKey);
fnCallBack(oNewContext);
}, function() {
fnCallBack(null); // error - notify to recreate contexts
});
} else {
fnCallBack(null); // error - notify to recreate contexts
}
}
return undefined;
};
/**
* Checks if data based on <code>select</code>, <code>expand</code> parameters is already loaded
* or not. In case it couldn't be found we should reload the data so we return true.
*
* @param {string} sFullPath The path to the data
* @param {object} oData The data to check for completeness
* @param {object} [mParameters] Value of <code>select</code> and/or <code>expand</code>
*
* @returns {boolean} Whether a reload is needed
*/
ODataModel.prototype._isReloadNeeded = function(sFullPath, oData, mParameters) {
var oDataObject, i, sNavProps, sPropKey, bReloadNeeded, sSelectProps,
aNavProps = [],
aSelectProps = [];
// no valid path --> no reload
if (!sFullPath) {
return false;
}
// no data --> reload needed
if (!oData) {
return true;
}
//Split the Navigation-Properties (or multi-level chains) which should be expanded
if (mParameters && mParameters["expand"]) {
sNavProps = mParameters["expand"].replace(/\s/g, "");
aNavProps = sNavProps.split(',');
}
//Split the Navigation properties again, if there are multi-level properties chained together by "/"
//The resulting aNavProps array will look like this: ["a", ["b", "c/d/e"], ["f", "g/h"], "i"]
if (aNavProps) {
for (i = 0; i < aNavProps.length; i++) {
var chainedPropIndex = aNavProps[i].indexOf("/");
if (chainedPropIndex !== -1) {
//cut of the first nav property of the chain
var chainedPropFirst = aNavProps[i].slice(0, chainedPropIndex);
var chainedPropRest = aNavProps[i].slice(chainedPropIndex + 1);
//keep track of the newly splitted nav prop chain
aNavProps[i] = [chainedPropFirst, chainedPropRest];
}
}
}
//Iterate all nav props and follow the given expand-chain
for (i = 0; i < aNavProps.length; i++) {
var navProp = aNavProps[i];
//check if the navProp was split into multiple parts (meaning it's an array)
//e.g. ["Orders", "Products/Suppliers"]
if (Array.isArray(navProp)) {
var oFirstNavProp = oData[navProp[0]];
var sNavPropRest = navProp[1];
//first nav prop in the chain is either undefined or deferred -> reload needed
if (!oFirstNavProp || (oFirstNavProp && oFirstNavProp.__deferred)) {
return true;
} else if (oFirstNavProp) {
if (oFirstNavProp.__list && oFirstNavProp.__list.length > 0) {
//Follow all keys in the __list collection by recursively calling
//this function to check if all linked properties are loaded.
//This is basically a depth-first search.
for (var iNavIndex = 0; iNavIndex < oFirstNavProp.__list.length;
iNavIndex++) {
sPropKey = "/" + oFirstNavProp.__list[iNavIndex];
oDataObject = this.getObject(sPropKey);
bReloadNeeded =
this._isReloadNeeded(sPropKey, oDataObject, {expand: sNavPropRest});
if (bReloadNeeded) {
//if a single nav-prop path is not loaded -> reload needed
return true;
}
}
} else if (oFirstNavProp.__ref) {
//the first nav-prop is not a __list
//but only a reference to a single entry (__ref)
sPropKey = "/" + oFirstNavProp.__ref;
oDataObject = this.getObject(sPropKey);
bReloadNeeded =
this._isReloadNeeded(sPropKey, oDataObject, {expand: sNavPropRest});
if (bReloadNeeded) {
return true;
}
}
}
} else if (oData[navProp] =