@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
455 lines (413 loc) • 15.4 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.
*/
/**
* JSON-based DataBinding
*
* @namespace
* @name sap.ui.model.json
* @public
*/
// Provides the JSON object based model implementation
sap.ui.define([
'sap/ui/model/ClientModel',
'sap/ui/model/Context',
'./JSONListBinding',
'./JSONPropertyBinding',
'./JSONTreeBinding',
"sap/base/Log",
"sap/ui/thirdparty/jquery",
"sap/base/util/isPlainObject"
],
function(
ClientModel,
Context,
JSONListBinding,
JSONPropertyBinding,
JSONTreeBinding,
Log,
jQuery,
isPlainObject
) {
"use strict";
/**
* Constructor for a new JSONModel.
*
* The observation feature is experimental! When observation is activated, the application can directly change the
* JS objects without the need to call setData, setProperty or refresh. Observation does only work for existing
* properties in the JSON, it cannot detect new properties or new array entries.
*
* @param {object|string} [oData] Either the URL where to load the JSON from or a JS object
* @param {boolean} [bObserve] Whether to observe the JSON data for property changes (experimental)
*
* @class
* Model implementation for JSON format
*
* @extends sap.ui.model.ClientModel
*
* @author SAP SE
* @version 1.87.1
* @public
* @alias sap.ui.model.json.JSONModel
*/
var JSONModel = ClientModel.extend("sap.ui.model.json.JSONModel", /** @lends sap.ui.model.json.JSONModel.prototype */ {
constructor : function(oData, bObserve) {
this.pSequentialImportCompleted = Promise.resolve();
ClientModel.apply(this, arguments);
this.bObserve = bObserve;
if (oData && typeof oData == "object") {
this.setData(oData);
}
},
metadata : {
publicMethods : ["setJSON", "getJSON"]
}
});
/**
* Sets the data, passed as a JS object tree, to the model.
*
* @param {object} oData the data to set on the model
* @param {boolean} [bMerge=false] whether to merge the data instead of replacing it
*
* @public
*/
JSONModel.prototype.setData = function(oData, bMerge){
if (bMerge) {
// do a deep copy
this.oData = jQuery.extend(true, Array.isArray(this.oData) ? [] : {}, this.oData, oData);
} else {
this.oData = oData;
}
if (this.bObserve) {
this.observeData();
}
this.checkUpdate();
};
/**
* Recursively iterates the JSON data and adds setter functions for the properties
*
* @private
*/
JSONModel.prototype.observeData = function(){
var that = this;
function createGetter(vValue) {
return function() {
return vValue;
};
}
function createSetter(oObject, sName) {
return function(vValue) {
// Newly added data needs to be observed to be included
observeRecursive(vValue, oObject, sName);
that.checkUpdate();
};
}
function createProperty(oObject, sName, vValue) {
// Do not create getter/setter for function references
if (typeof vValue == "function"){
oObject[sName] = vValue;
} else {
Object.defineProperty(oObject, sName, {
get: createGetter(vValue),
set: createSetter(oObject, sName)
});
}
}
function observeRecursive(oObject, oParentObject, sName) {
if (Array.isArray(oObject)) {
for (var i = 0; i < oObject.length; i++) {
observeRecursive(oObject[i], oObject, i);
}
} else if (isPlainObject(oObject)) {
for (var i in oObject) {
observeRecursive(oObject[i], oObject, i);
}
}
if (oParentObject) {
createProperty(oParentObject, sName, oObject);
}
}
observeRecursive(this.oData);
};
/**
* Sets the data, passed as a string in JSON format, to the model.
*
* @param {string} sJSON the JSON data to set on the model
* @param {boolean} [bMerge=false] whether to merge the data instead of replacing it
*
* @public
*/
JSONModel.prototype.setJSON = function(sJSON, bMerge){
var oJSONData;
try {
oJSONData = JSON.parse(sJSON + "");
this.setData(oJSONData, bMerge);
} catch (e) {
Log.fatal("The following problem occurred: JSON parse Error: " + e);
this.fireParseError({url : "", errorCode : -1,
reason : "", srcText : e, line : -1, linepos : -1, filepos : -1});
}
};
/**
* Serializes the current JSON data of the model into a string.
* Note: May not work in Internet Explorer 8 because of lacking JSON support (works only if IE 8 mode is enabled)
*
* @return {string} the JSON data serialized as string
* @public
*/
JSONModel.prototype.getJSON = function(){
return JSON.stringify(this.oData);
};
/**
* Load JSON-encoded data from the server using a GET HTTP request and store the resulting JSON data in the model.
* Note: Due to browser security restrictions, most "Ajax" requests are subject to the same origin policy,
* the request can not successfully retrieve data from a different domain, subdomain, or protocol.
*
* @param {string} sURL A string containing the URL to which the request is sent.
* @param {object | string} [oParameters] A map or string that is sent to the server with the request.
* Data that is sent to the server is appended to the URL as a query string.
* If the value of the data parameter is an object (map), it is converted to a string and
* url-encoded before it is appended to the URL.
* @param {boolean} [bAsync=true] By default, all requests are sent asynchronous.
* <b>Do not use <code>bAsync=false</code></b> because synchronous requests may temporarily lock
* the browser, disabling any actions while the request is active. Cross-domain requests do not
* support synchronous operation.
* @param {string} [sType=GET] The type of request to make ("POST" or "GET"), default is "GET".
* Note: Other HTTP request methods, such as PUT and DELETE, can also be used here, but
* they are not supported by all browsers.
* @param {boolean} [bMerge=false] Whether the data should be merged instead of replaced
* @param {boolean} [bCache=true] Disables caching if set to false. Default is true.
* @param {object} [mHeaders] An object of additional header key/value pairs to send along with the request
*
* @return {Promise|undefined} in case bAsync is set to true a Promise is returned; this promise resolves/rejects based on the request status
* @public
*/
JSONModel.prototype.loadData = function(sURL, oParameters, bAsync, sType, bMerge, bCache, mHeaders){
var pImportCompleted;
bAsync = (bAsync !== false);
sType = sType || "GET";
bCache = bCache === undefined ? this.bCache : bCache;
this.fireRequestSent({url : sURL, type : sType, async : bAsync, headers: mHeaders,
info : "cache=" + bCache + ";bMerge=" + bMerge, infoObject: {cache : bCache, merge : bMerge}});
var fnSuccess = function(oData) {
if (!oData) {
Log.fatal("The following problem occurred: No data was retrieved by service: " + sURL);
}
this.setData(oData, bMerge);
this.fireRequestCompleted({url : sURL, type : sType, async : bAsync, headers: mHeaders,
info : "cache=" + bCache + ";bMerge=" + bMerge, infoObject: {cache : bCache, merge : bMerge}, success: true});
}.bind(this);
var fnError = function(oParams, sTextStatus){
// the textStatus is either passed by jQuery via arguments,
// or by us from a promise reject() in the async case
var sMessage = sTextStatus || oParams.textStatus;
var oParams = bAsync ? oParams.request : oParams;
var iStatusCode = oParams.status;
var sStatusText = oParams.statusText;
var sResponseText = oParams.responseText;
var oError = {
message : sMessage,
statusCode : iStatusCode,
statusText : sStatusText,
responseText : sResponseText
};
Log.fatal("The following problem occurred: " + sMessage, sResponseText + "," + iStatusCode + "," + sStatusText);
this.fireRequestCompleted({url : sURL, type : sType, async : bAsync, headers: mHeaders,
info : "cache=" + bCache + ";bMerge=" + bMerge, infoObject: {cache : bCache, merge : bMerge}, success: false, errorobject: oError});
this.fireRequestFailed(oError);
if (bAsync) {
return Promise.reject(oError);
}
}.bind(this);
var _loadData = function(fnSuccess, fnError) {
this._ajax({
url: sURL,
async: bAsync,
dataType: 'json',
cache: bCache,
data: oParameters,
headers: mHeaders,
type: sType,
success: fnSuccess,
error: fnError
});
}.bind(this);
if (bAsync) {
pImportCompleted = new Promise(function(resolve, reject) {
var fnReject = function(oXMLHttpRequest, sTextStatus, oError) {
reject({request: oXMLHttpRequest, textStatus: sTextStatus, error: oError});
};
_loadData(resolve, fnReject);
});
// chain the existing loadData calls, so the import is done sequentially
var pReturn = this.pSequentialImportCompleted.then(function() {
return pImportCompleted.then(fnSuccess, fnError);
});
// attach exception/rejection handler, so the internal import promise always resolves
this.pSequentialImportCompleted = pReturn.catch(function(oError) {
Log.error("Loading of data failed: " + oError.stack);
});
// return chained loadData promise (sequential imports)
// but without a catch handler, so the application can also is notified about request failures
return pReturn;
} else {
_loadData(fnSuccess, fnError);
}
};
/**
* Returns a Promise of the current data-loading state.
* Every currently running {@link sap.ui.model.json.JSONModel#loadData} call is respected by the returned Promise.
* This also includes a potential loadData call from the JSONModel's constructor in case a URL was given.
* The data-loaded Promise will resolve once all running requests have finished.
* Only request, which have been queued up to the point of calling
* this function will be respected by the returned Promise.
*
* @return {Promise} a Promise, which resolves if all pending data-loading requests have finished
* @public
*/
JSONModel.prototype.dataLoaded = function() {
return this.pSequentialImportCompleted;
};
/**
* @see sap.ui.model.Model.prototype.bindProperty
*
*/
JSONModel.prototype.bindProperty = function(sPath, oContext, mParameters) {
var oBinding = new JSONPropertyBinding(this, sPath, oContext, mParameters);
return oBinding;
};
/**
* @see sap.ui.model.Model.prototype.bindList
*
*/
JSONModel.prototype.bindList = function(sPath, oContext, aSorters, aFilters, mParameters) {
var oBinding = new JSONListBinding(this, sPath, oContext, aSorters, aFilters, mParameters);
return oBinding;
};
/**
* @see sap.ui.model.Model.prototype.bindTree
*
* @param {object}
* [mParameters=null] additional model specific parameters (optional)
* If the mParameter <code>arrayNames</code> is specified with an array of string names this names will be checked against the tree data structure
* and the found data in this array is included in the tree but only if also the parent array is included.
* If this parameter is not specified then all found arrays in the data structure are bound.
* If the tree data structure doesn't contain an array you don't have to specify this parameter.
*
*/
JSONModel.prototype.bindTree = function(sPath, oContext, aFilters, mParameters, aSorters) {
var oBinding = new JSONTreeBinding(this, sPath, oContext, aFilters, mParameters, aSorters);
return oBinding;
};
/**
* Sets a new value for the given property <code>sPropertyName</code> in the model.
* If the model value changed all interested parties are informed.
*
* @param {string} sPath path of the property to set
* @param {any} oValue value to set the property to
* @param {object} [oContext=null] the context which will be used to set the property
* @param {boolean} [bAsyncUpdate] whether to update other bindings dependent on this property asynchronously
* @return {boolean} true if the value was set correctly and false if errors occurred like the entry was not found.
* @public
*/
JSONModel.prototype.setProperty = function(sPath, oValue, oContext, bAsyncUpdate) {
var sResolvedPath = this.resolve(sPath, oContext),
iLastSlash, sObjectPath, sProperty;
// return if path / context is invalid
if (!sResolvedPath) {
return false;
}
// If data is set on root, call setData instead
if (sResolvedPath == "/") {
this.setData(oValue);
return true;
}
iLastSlash = sResolvedPath.lastIndexOf("/");
// In case there is only one slash at the beginning, sObjectPath must contain this slash
sObjectPath = sResolvedPath.substring(0, iLastSlash || 1);
sProperty = sResolvedPath.substr(iLastSlash + 1);
var oObject = this._getObject(sObjectPath);
if (oObject) {
oObject[sProperty] = oValue;
this.checkUpdate(false, bAsyncUpdate);
return true;
}
return false;
};
/**
* Returns the value for the property with the given path and context.
*
* @param {string} sPath
* The path to the property
* @param {sap.ui.model.Context} [oContext=null]
* The context which will be used to retrieve the property
* @return {any}
* The value of the property. If the property is not found, <code>null</code> or
* <code>undefined</code> is returned.
* @public
*/
JSONModel.prototype.getProperty = function(sPath, oContext) {
return this._getObject(sPath, oContext);
};
/**
* Returns the value for the property with the given path and context.
*
* @param {string} sPath
* The path to the property
* @param {object|sap.ui.model.Context} [oContext]
* The context or a JSON object
* @returns {any}
* The value of the property. If the property path derived from the given path and context is
* absolute (starts with a "/") but does not lead to a property in the data structure,
* <code>undefined</code> is returned. If the property path is not absolute, <code>null</code>
* is returned.
*
* Note: If a JSON object is given instead of a context, the value of the property is taken
* from the JSON object. If the given path does not lead to a property, <code>undefined</code>
* is returned. If the given path represents a falsy JavaScript value, the given JSON object
* is returned.
* @private
*/
JSONModel.prototype._getObject = function (sPath, oContext) {
var oNode = this.isLegacySyntax() ? this.oData : null;
if (oContext instanceof Context) {
oNode = this._getObject(oContext.getPath());
} else if (oContext != null) {
oNode = oContext;
}
if (!sPath) {
return oNode;
}
var aParts = sPath.split("/"),
iIndex = 0;
if (!aParts[0]) {
// absolute path starting with slash
oNode = this.oData;
iIndex++;
}
while (oNode && aParts[iIndex]) {
oNode = oNode[aParts[iIndex]];
iIndex++;
}
return oNode;
};
JSONModel.prototype.isList = function(sPath, oContext) {
var sAbsolutePath = this.resolve(sPath, oContext);
return Array.isArray(this._getObject(sAbsolutePath));
};
/**
* Sets the meta model associated with this model
*
* @private
* @param {sap.ui.model.MetaModel} oMetaModel the meta model associated with this model
*/
JSONModel.prototype._setMetaModel = function(oMetaModel) {
this._oMetaModel = oMetaModel;
};
JSONModel.prototype.getMetaModel = function() {
return this._oMetaModel;
};
return JSONModel;
});