summaryrefslogtreecommitdiff
path: root/includes/js/dojox/data/QueryReadStore.js
blob: 95af1667d7604f2891f2fff54199db09bcec2f05 (plain)
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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
if(!dojo._hasResource["dojox.data.QueryReadStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.data.QueryReadStore"] = true;
dojo.provide("dojox.data.QueryReadStore");

dojo.require("dojo.string");

dojo.declare("dojox.data.QueryReadStore",
	null,
	{
		//	summary:
		//		This class provides a store that is mainly intended to be used
		//		for loading data dynamically from the server, used i.e. for
		//		retreiving chunks of data from huge data stores on the server (by server-side filtering!).
		//		Upon calling the fetch() method of this store the data are requested from
		//		the server if they are not yet loaded for paging (or cached).
		//
		//		For example used for a combobox which works on lots of data. It
		//		can be used to retreive the data partially upon entering the
		//		letters "ac" it returns only items like "action", "acting", etc.
		//
		// note:
		//		The field name "id" in a query is reserved for looking up data
		//		by id. This is necessary as before the first fetch, the store
		//		has no way of knowing which field the server will declare as
		//		identifier.
		//
		//	examples:
		// |	// The parameter "query" contains the data that are sent to the server.
		// |	var store = new dojox.data.QueryReadStore({url:'/search.php'});
		// |	store.fetch({query:{name:'a'}, queryOptions:{ignoreCase:false}});
		//
		// |	// Since "serverQuery" is given, it overrules and those data are
		// |	// sent to the server.
		// |	var store = new dojox.data.QueryReadStore({url:'/search.php'});
		// |	store.fetch({serverQuery:{name:'a'}, queryOptions:{ignoreCase:false}});
		//
		// |	<div dojoType="dojox.data.QueryReadStore"
		// |		jsId="store2"
		// |		url="../tests/stores/QueryReadStore.php"
		// |		requestMethod="post"></div>
		// |	<div dojoType="dojox.grid.data.DojoData"
		// |		jsId="model2"
		// |		store="store2"
		// |		sortFields="[{attribute: 'name', descending: true}]"
		// |		rowsPerPage="30"></div>
		// |	<div dojoType="dojox.Grid" id="grid2"
		// |		model="model2"
		// |		structure="gridLayout"
		// |		style="height:300px; width:800px;"></div>
	
		//
		//	todo:
		//		- there is a bug in the paging, when i set start:2, count:5 after an initial fetch() and doClientPaging:true
		//		  it returns 6 elemetns, though count=5, try it in QueryReadStore.html
		//		- add optional caching
		//		- when the first query searched for "a" and the next for a subset of
		//		  the first, i.e. "ab" then we actually dont need a server request, if
		//		  we have client paging, we just need to filter the items we already have
		//		  that might also be tooo much logic
		
		url:"",
		requestMethod:"get",
		//useCache:false,
		
		// We use the name in the errors, once the name is fixed hardcode it, may be.
		_className:"dojox.data.QueryReadStore",
		
		// This will contain the items we have loaded from the server.
		// The contents of this array is optimized to satisfy all read-api requirements
		// and for using lesser storage, so the keys and their content need some explaination:
		// 		this._items[0].i - the item itself 
		//		this._items[0].r - a reference to the store, so we can identify the item
		//			securly. We set this reference right after receiving the item from the
		//			server.
		_items:[],
		
		// Store the last query that triggered xhr request to the server.
		// So we can compare if the request changed and if we shall reload 
		// (this also depends on other factors, such as is caching used, etc).
		_lastServerQuery:null,
		
		
		// Store a hash of the last server request. Actually I introduced this
		// for testing, so I can check if no unnecessary requests were issued for
		// client-side-paging.
		lastRequestHash:null,
		
		// summary:
		//		By default every request for paging is sent to the server.
		doClientPaging:false,
	
		// summary:
		//		By default all the sorting is done serverside before the data is returned
		//		which is the proper place to be doing it for really large datasets.
		doClientSorting:false,
	
		// Items by identify for Identify API
		_itemsByIdentity:null,
		
		// Identifier used
		_identifier:null,
	
		_features: {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true},
	
		_labelAttr: "label",
		
		constructor: function(/* Object */ params){
			dojo.mixin(this,params);
		},
		
		getValue: function(/* item */ item, /* attribute-name-string */ attribute, /* value? */ defaultValue){
			//	According to the Read API comments in getValue() and exception is
			//	thrown when an item is not an item or the attribute not a string!
			this._assertIsItem(item);
			if (!dojo.isString(attribute)) {
				throw new Error(this._className+".getValue(): Invalid attribute, string expected!");
			}
			if(!this.hasAttribute(item, attribute)){
				// read api says: return defaultValue "only if *item* does not have a value for *attribute*." 
				// Is this the case here? The attribute doesn't exist, but a defaultValue, sounds reasonable.
				if(defaultValue){
					return defaultValue;
				}
				console.log(this._className+".getValue(): Item does not have the attribute '"+attribute+"'.");
			}
			return item.i[attribute];
		},
		
		getValues: function(/* item */ item, /* attribute-name-string */ attribute){
			this._assertIsItem(item);
			var ret = [];
			if(this.hasAttribute(item, attribute)){
				ret.push(item.i[attribute]);
			}
			return ret;
		},
		
		getAttributes: function(/* item */ item){
			this._assertIsItem(item);
			var ret = [];
			for(var i in item.i){
				ret.push(i);
			}
			return ret;
		},
	
		hasAttribute: function(/* item */ item,	/* attribute-name-string */ attribute) {
			//	summary: 
			//		See dojo.data.api.Read.hasAttribute()
			return this.isItem(item) && typeof item.i[attribute]!="undefined";
		},
		
		containsValue: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ value){
			var values = this.getValues(item, attribute);
			var len = values.length;
			for(var i=0; i<len; i++){
				if(values[i]==value){
					return true;
				}
			}
			return false;
		},
		
		isItem: function(/* anything */ something){
			// Some basic tests, that are quick and easy to do here.
			// >>> var store = new dojox.data.QueryReadStore({});
			// >>> store.isItem("");
			// false
			//
			// >>> var store = new dojox.data.QueryReadStore({});
			// >>> store.isItem({});
			// false
			//
			// >>> var store = new dojox.data.QueryReadStore({});
			// >>> store.isItem(0);
			// false
			//
			// >>> var store = new dojox.data.QueryReadStore({});
			// >>> store.isItem({name:"me", label:"me too"});
			// false
			//
			if(something){
				return typeof something.r!="undefined" && something.r==this;
			}
			return false;
		},
		
		isItemLoaded: function(/* anything */ something) {
			// Currently we dont have any state that tells if an item is loaded or not
			// if the item exists its also loaded.
			// This might change when we start working with refs inside items ...
			return this.isItem(something);
		},
	
		loadItem: function(/* object */ args){
			if(this.isItemLoaded(args.item)){
				return;
			}
			// Actually we have nothing to do here, or at least I dont know what to do here ...
		},
	
		fetch:function(/* Object? */ request){
			//	summary:
			//		See dojo.data.util.simpleFetch.fetch() this is just a copy and I adjusted
			//		only the paging, since it happens on the server if doClientPaging is
			//		false, thx to http://trac.dojotoolkit.org/ticket/4761 reporting this.
			//		Would be nice to be able to use simpleFetch() to reduce copied code,
			//		but i dont know how yet. Ideas please!
			request = request || {};
			if(!request.store){
				request.store = this;
			}
			var self = this;
		
			var _errorHandler = function(errorData, requestObject){
				if(requestObject.onError){
					var scope = requestObject.scope || dojo.global;
					requestObject.onError.call(scope, errorData, requestObject);
				}
			};
		
			var _fetchHandler = function(items, requestObject, numRows){
				var oldAbortFunction = requestObject.abort || null;
				var aborted = false;
				
				var startIndex = requestObject.start?requestObject.start:0;
				if (self.doClientPaging==false) {
					// For client paging we dont need no slicing of the result.
					startIndex = 0;
				}
				var endIndex   = requestObject.count?(startIndex + requestObject.count):items.length;
		
				requestObject.abort = function(){
					aborted = true;
					if(oldAbortFunction){
						oldAbortFunction.call(requestObject);
					}
				};
		
				var scope = requestObject.scope || dojo.global;
				if(!requestObject.store){
					requestObject.store = self;
				}
				if(requestObject.onBegin){
					requestObject.onBegin.call(scope, numRows, requestObject);
				}
				if(requestObject.sort && this.doClientSorting){
					items.sort(dojo.data.util.sorter.createSortFunction(requestObject.sort, self));
				}
				if(requestObject.onItem){
					for(var i = startIndex; (i < items.length) && (i < endIndex); ++i){
						var item = items[i];
						if(!aborted){
							requestObject.onItem.call(scope, item, requestObject);
						}
					}
				}
				if(requestObject.onComplete && !aborted){
					var subset = null;
					if (!requestObject.onItem) {
						subset = items.slice(startIndex, endIndex);
					}
					requestObject.onComplete.call(scope, subset, requestObject);   
				}
			};
			this._fetchItems(request, _fetchHandler, _errorHandler);
			return request;	// Object
		},
	
		getFeatures: function(){
			return this._features;
		},
	
		close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
			// I have no idea if this is really needed ... 
		},
	
		getLabel: function(/* item */ item){
			//	summary:
			//		See dojo.data.api.Read.getLabel()
			if(this._labelAttr && this.isItem(item)){
				return this.getValue(item, this._labelAttr); //String
			}
			return undefined; //undefined
		},
	
		getLabelAttributes: function(/* item */ item){
			//	summary:
			//		See dojo.data.api.Read.getLabelAttributes()
			if(this._labelAttr){
				return [this._labelAttr]; //array
			}
			return null; //null
		},
		
		_fetchItems: function(request, fetchHandler, errorHandler){
			//	summary:
			// 		The request contains the data as defined in the Read-API.
			// 		Additionally there is following keyword "serverQuery".
			//
			//	The *serverQuery* parameter, optional.
			//		This parameter contains the data that will be sent to the server.
			//		If this parameter is not given the parameter "query"'s
			//		data are sent to the server. This is done for some reasons:
			//		- to specify explicitly which data are sent to the server, they
			//		  might also be a mix of what is contained in "query", "queryOptions"
			//		  and the paging parameters "start" and "count" or may be even
			//		  completely different things.
			//		- don't modify the request.query data, so the interface using this
			//		  store can rely on unmodified data, as the combobox dijit currently
			//		  does it, it compares if the query has changed
			//		- request.query is required by the Read-API
			//
			// 		I.e. the following examples might be sent via GET:
			//		  fetch({query:{name:"abc"}, queryOptions:{ignoreCase:true}})
			//		  the URL will become:   /url.php?name=abc
			//
			//		  fetch({serverQuery:{q:"abc", c:true}, query:{name:"abc"}, queryOptions:{ignoreCase:true}})
			//		  the URL will become:   /url.php?q=abc&c=true
			//		  // The serverQuery-parameter has overruled the query-parameter
			//		  // but the query parameter stays untouched, but is not sent to the server!
			//		  // The serverQuery contains more data than the query, so they might differ!
			//
	
			var serverQuery = request.serverQuery || request.query || {};
			//Need to add start and count
			if(!this.doClientPaging){
				serverQuery.start = request.start || 0;
				// Count might not be sent if not given.
				if (request.count) {
					serverQuery.count = request.count;
				}
			}
			if(!this.doClientSorting){
				if(request.sort){
					var sort = request.sort[0];
					if(sort && sort.attribute){
						var sortStr = sort.attribute;
						if(sort.descending){
							sortStr = "-" + sortStr;
						}
						serverQuery.sort = sortStr;
					}
				}
			}
			// Compare the last query and the current query by simply json-encoding them,
			// so we dont have to do any deep object compare ... is there some dojo.areObjectsEqual()???
			if(this.doClientPaging && this._lastServerQuery!==null &&
				dojo.toJson(serverQuery)==dojo.toJson(this._lastServerQuery)
				){
				fetchHandler(this._items, request);
			}else{
				var xhrFunc = this.requestMethod.toLowerCase()=="post" ? dojo.xhrPost : dojo.xhrGet;
				var xhrHandler = xhrFunc({url:this.url, handleAs:"json-comment-optional", content:serverQuery});
				xhrHandler.addCallback(dojo.hitch(this, function(data){
					data = this._filterResponse(data);
					if (data.label){
						this._labelAttr = data.label;
					}
					var numRows = data.numRows || -1;
	
					this._items = [];
					// Store a ref to "this" in each item, so we can simply check if an item
					// really origins form here (idea is from ItemFileReadStore, I just don't know
					// how efficient the real storage use, garbage collection effort, etc. is).
					dojo.forEach(data.items,function(e){ 
						this._items.push({i:e, r:this}); 
					},this); 
					
					var identifier = data.identifier;
					this._itemsByIdentity = {};
					if(identifier){
						this._identifier = identifier;
						for(i = 0; i < this._items.length; ++i){
							var item = this._items[i].i;
							var identity = item[identifier];
							if(!this._itemsByIdentity[identity]){
								this._itemsByIdentity[identity] = item;
							}else{
								throw new Error(this._className+":  The json data as specified by: [" + this.url + "] is malformed.  Items within the list have identifier: [" + identifier + "].  Value collided: [" + identity + "]");
							}
						}
					}else{
						this._identifier = Number;
						for(i = 0; i < this._items.length; ++i){
							this._items[i].n = i;
						}
					}
					
					// TODO actually we should do the same as dojo.data.ItemFileReadStore._getItemsFromLoadedData() to sanitize
					// (does it really sanititze them) and store the data optimal. should we? for security reasons???
					numRows = (numRows === -1) ? this._items.length : numRows;
					fetchHandler(this._items, request, numRows);
				}));
				xhrHandler.addErrback(function(error){
					errorHandler(error, request);
				});
				// Generate the hash using the time in milliseconds and a randon number.
				// Since Math.randon() returns something like: 0.23453463, we just remove the "0."
				// probably just for esthetic reasons :-).
				this.lastRequestHash = new Date().getTime()+"-"+String(Math.random()).substring(2);
				this._lastServerQuery = dojo.mixin({}, serverQuery);
			}
		},
		
		_filterResponse: function(data){
			//	summary:
			//		If the data from servers needs to be processed before it can be processed by this
			//		store, then this function should be re-implemented in subclass. This default 
			//		implementation just return the data unchanged.
			//	data:
			//		The data received from server
			return data;
		},
	
		_assertIsItem: function(/* item */ item){
			//	summary:
			//		It throws an error if item is not valid, so you can call it in every method that needs to
			//		throw an error when item is invalid.
			//	item: 
			//		The item to test for being contained by the store.
			if(!this.isItem(item)){
				throw new Error(this._className+": Invalid item argument.");
			}
		},
	
		_assertIsAttribute: function(/* attribute-name-string */ attribute){
			//	summary:
			//		This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
			//	attribute: 
			//		The attribute to test for being contained by the store.
			if(typeof attribute !== "string"){ 
				throw new Error(this._className+": Invalid attribute argument ('"+attribute+"').");
			}
		},
	
		fetchItemByIdentity: function(/* Object */ keywordArgs){
			//	summary: 
			//		See dojo.data.api.Identity.fetchItemByIdentity()
	
			// See if we have already loaded the item with that id
			// In case there hasn't been a fetch yet, _itemsByIdentity is null
			// and thus a fetch will be triggered below.
			if(this._itemsByIdentity){
				var item = this._itemsByIdentity[keywordArgs.identity];
				if(!(item === undefined)){
					if(keywordArgs.onItem){
						var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
						keywordArgs.onItem.call(scope, {i:item, r:this});
					}
					return;
				}
			}
	
			// Otherwise we need to go remote
			// Set up error handler
			var _errorHandler = function(errorData, requestObject){
				var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
				if(keywordArgs.onError){
					keywordArgs.onError.call(scope, error);
				}
			};
			
			// Set up fetch handler
			var _fetchHandler = function(items, requestObject){
				var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
				try{
					// There is supposed to be only one result
					var item = null;
					if(items && items.length == 1){
						item = items[0];
					}
					
					// If no item was found, item is still null and we'll
					// fire the onItem event with the null here
					if(keywordArgs.onItem){
						keywordArgs.onItem.call(scope, item);
					}
				}catch(error){
					if(keywordArgs.onError){
						keywordArgs.onError.call(scope, error);
					}
				}
			};
			
			// Construct query
			var request = {serverQuery:{id:keywordArgs.identity}};
			
			// Dispatch query
			this._fetchItems(request, _fetchHandler, _errorHandler);
		},
		
		getIdentity: function(/* item */ item){
			//	summary: 
			//		See dojo.data.api.Identity.getIdentity()
			var identifier = null;
			if(this._identifier === Number){
				identifier = item.n; // Number
			}else{
				identifier = item.i[this._identifier];
			}
			return identifier;
		},
		
		getIdentityAttributes: function(/* item */ item){
			//	summary:
			//		See dojo.data.api.Identity.getIdentityAttributes()
			return [this._identifier];
		}
	}
);

}