/
JSONModel.js
458 lines (415 loc) · 15 KB
/
JSONModel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
/*!
* ${copyright}
*/
/*eslint-disable max-len */
/**
* JSON-based DataBinding
*
* @namespace
* @name sap.ui.model.json
* @public
*/
// Provides the JSON object based model implementation
sap.ui.define([
"./JSONListBinding",
"./JSONPropertyBinding",
"./JSONTreeBinding",
"sap/base/Log",
"sap/base/util/deepExtend",
"sap/base/util/isPlainObject",
"sap/ui/model/ClientModel",
"sap/ui/model/Context"
], function(JSONListBinding, JSONPropertyBinding, JSONTreeBinding, Log, deepExtend, isPlainObject,
ClientModel, Context) {
"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 the JSON format.
*
* This model is not prepared to be inherited from.
*
* @extends sap.ui.model.ClientModel
*
* @author SAP SE
* @version ${version}
* @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 = deepExtend(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) {
var i;
if (Array.isArray(oObject)) {
for (i = 0; i < oObject.length; i++) {
observeRecursive(oObject[i], oObject, i);
}
} else if (isPlainObject(oObject)) {
for (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.
*
* @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 oParameters = bAsync ? oParams.request : oParams;
var iStatusCode = oParameters.status;
var sStatusText = oParameters.statusText;
var sResponseText = oParameters.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);
}
return undefined;
}.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);
return undefined;
}
};
/**
* 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]
* Additional model specific parameters; if the mParameter <code>arrayNames</code> is
* specified with an array of string names these names will be checked against the tree data
* structure and the found data in this array is included in the tree, but only if the parent
* array is also 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, this
* parameter doesn't need to be specified
*
*/
JSONModel.prototype.bindTree = function(sPath, oContext, aFilters, mParameters, aSorters) {
var oBinding = new JSONTreeBinding(this, sPath, oContext, aFilters, mParameters, aSorters);
return oBinding;
};
/**
* Sets <code>oValue</code> as new value for the property defined by the given
* <code>sPath</code> and <code>oContext</code>. Once the new model value has been set, all
* interested parties are informed.
*
* @param {string} sPath
* The path of the property to set
* @param {any} oValue
* The new value to be set for this property
* @param {sap.ui.model.Context} [oContext]
* The context used to set the property
* @param {boolean} [bAsyncUpdate]
* Whether to update other bindings dependent on this property asynchronously
* @return {boolean}
* <code>true</code> if the value was set correctly, and <code>false</code> if errors
* occurred, for example if 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]
* 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;
});