source: Dev/trunk/src/client/dojox/data/QueryReadStore.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 17.9 KB
Line 
1define(["dojo", "dojox", "dojo/data/util/sorter", "dojo/string"], function(dojo, dojox) {
2
3        return dojo.declare("dojox.data.QueryReadStore", null, {
4                // summary:
5                //              This class provides a store that is mainly intended to be used
6                //              for loading data dynamically from the server, used i.e. for
7                //              retrieving chunks of data from huge data stores on the server (by server-side filtering!).
8                //              Upon calling the fetch() method of this store the data are requested from
9                //              the server if they are not yet loaded for paging (or cached).
10                //
11                //              For example used for a combobox which works on lots of data. It
12                //              can be used to retrieve the data partially upon entering the
13                //              letters "ac" it returns only items like "action", "acting", etc.
14                //
15                //              note:
16                //              The field name "id" in a query is reserved for looking up data
17                //              by id. This is necessary as before the first fetch, the store
18                //              has no way of knowing which field the server will declare as
19                //              identifier.
20                //
21                // example:
22                // |    // The parameter "query" contains the data that are sent to the server.
23                // |    var store = new dojox.data.QueryReadStore({url:'/search.php'});
24                // |    store.fetch({query:{name:'a'}, queryOptions:{ignoreCase:false}});
25                //
26                // |    // Since "serverQuery" is given, it overrules and those data are
27                // |    // sent to the server.
28                // |    var store = new dojox.data.QueryReadStore({url:'/search.php'});
29                // |    store.fetch({serverQuery:{name:'a'}, queryOptions:{ignoreCase:false}});
30                //
31                // |    <div dojoType="dojox.data.QueryReadStore"
32                // |            jsId="store2"
33                // |            url="../tests/stores/QueryReadStore.php"
34                // |            requestMethod="post"></div>
35                // |    <div dojoType="dojox.grid.data.DojoData"
36                // |            jsId="model2"
37                // |            store="store2"
38                // |            sortFields="[{attribute: 'name', descending: true}]"
39                // |            rowsPerPage="30"></div>
40                // |    <div dojoType="dojox.Grid" id="grid2"
41                // |            model="model2"
42                // |            structure="gridLayout"
43                // |            style="height:300px; width:800px;"></div>
44
45                // todo:
46                //              - there is a bug in the paging, when i set start:2, count:5 after an initial fetch() and doClientPaging:true
47                //                it returns 6 elemetns, though count=5, try it in QueryReadStore.html
48                //              - add optional caching
49                //              - when the first query searched for "a" and the next for a subset of
50                //                the first, i.e. "ab" then we actually dont need a server request, if
51                //                we have client paging, we just need to filter the items we already have
52                //                that might also be tooo much logic
53               
54                url:"",
55                requestMethod:"get",
56                //useCache:false,
57               
58                // We use the name in the errors, once the name is fixed hardcode it, may be.
59                _className:"dojox.data.QueryReadStore",
60               
61                // This will contain the items we have loaded from the server.
62                // The contents of this array is optimized to satisfy all read-api requirements
63                // and for using lesser storage, so the keys and their content need some explaination:
64                //              this._items[0].i - the item itself
65                //              this._items[0].r - a reference to the store, so we can identify the item
66                //                      securely. We set this reference right after receiving the item from the
67                //                      server.
68                _items:[],
69               
70                // Store the last query that triggered xhr request to the server.
71                // So we can compare if the request changed and if we shall reload
72                // (this also depends on other factors, such as is caching used, etc).
73                _lastServerQuery:null,
74               
75                // Store how many rows we have so that we can pass it to a clientPaging handler
76                _numRows:-1,
77               
78                // Store a hash of the last server request. Actually I introduced this
79                // for testing, so I can check if no unnecessary requests were issued for
80                // client-side-paging.
81                lastRequestHash:null,
82               
83                // doClientPaging: Boolean
84                //              By default every request for paging is sent to the server.
85                doClientPaging:false,
86       
87                // doClientSorting: Boolean
88                //              By default all the sorting is done serverside before the data is returned
89                //              which is the proper place to be doing it for really large datasets.
90                doClientSorting:false,
91       
92                // Items by identify for Identify API
93                _itemsByIdentity:null,
94               
95                // Identifier used
96                _identifier:null,
97       
98                _features: {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true},
99       
100                _labelAttr: "label",
101               
102                constructor: function(/* Object */ params){
103                        dojo.mixin(this,params);
104                },
105               
106                getValue: function(/* item */ item, /* attribute-name-string */ attribute, /* value? */ defaultValue){
107                        //      According to the Read API comments in getValue() and exception is
108                        //      thrown when an item is not an item or the attribute not a string!
109                        this._assertIsItem(item);
110                        if(!dojo.isString(attribute)){
111                                throw new Error(this._className+".getValue(): Invalid attribute, string expected!");
112                        }
113                        if(!this.hasAttribute(item, attribute)){
114                                // read api says: return defaultValue "only if *item* does not have a value for *attribute*."
115                                // Is this the case here? The attribute doesn't exist, but a defaultValue, sounds reasonable.
116                                if(defaultValue){
117                                        return defaultValue;
118                                }
119                        }
120                        return item.i[attribute];
121                },
122               
123                getValues: function(/* item */ item, /* attribute-name-string */ attribute){
124                        this._assertIsItem(item);
125                        var ret = [];
126                        if(this.hasAttribute(item, attribute)){
127                                ret.push(item.i[attribute]);
128                        }
129                        return ret;
130                },
131               
132                getAttributes: function(/* item */ item){
133                        this._assertIsItem(item);
134                        var ret = [];
135                        for(var i in item.i){
136                                ret.push(i);
137                        }
138                        return ret;
139                },
140       
141                hasAttribute: function(/* item */ item, /* attribute-name-string */ attribute){
142                        // summary:
143                        //              See dojo/data/api/Read.hasAttribute()
144                        return this.isItem(item) && typeof item.i[attribute]!="undefined";
145                },
146               
147                containsValue: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ value){
148                        var values = this.getValues(item, attribute);
149                        var len = values.length;
150                        for(var i=0; i<len; i++){
151                                if(values[i] == value){
152                                        return true;
153                                }
154                        }
155                        return false;
156                },
157               
158                isItem: function(/* anything */ something){
159                        // Some basic tests, that are quick and easy to do here.
160                        // >>> var store = new dojox.data.QueryReadStore({});
161                        // >>> store.isItem("");
162                        // false
163                        //
164                        // >>> var store = new dojox.data.QueryReadStore({});
165                        // >>> store.isItem({});
166                        // false
167                        //
168                        // >>> var store = new dojox.data.QueryReadStore({});
169                        // >>> store.isItem(0);
170                        // false
171                        //
172                        // >>> var store = new dojox.data.QueryReadStore({});
173                        // >>> store.isItem({name:"me", label:"me too"});
174                        // false
175
176                        if(something){
177                                return typeof something.r != "undefined" && something.r == this;
178                        }
179                        return false;
180                },
181               
182                isItemLoaded: function(/* anything */ something){
183                        // Currently we dont have any state that tells if an item is loaded or not
184                        // if the item exists its also loaded.
185                        // This might change when we start working with refs inside items ...
186                        return this.isItem(something);
187                },
188       
189                loadItem: function(/* object */ args){
190                        if(this.isItemLoaded(args.item)){
191                                return;
192                        }
193                        // Actually we have nothing to do here, or at least I dont know what to do here ...
194                },
195       
196                fetch:function(/* Object? */ request){
197                        // summary:
198                        //              See dojo.data.util.simpleFetch.fetch() this is just a copy and I adjusted
199                        //              only the paging, since it happens on the server if doClientPaging is
200                        //              false, thx to http://trac.dojotoolkit.org/ticket/4761 reporting this.
201                        //              Would be nice to be able to use simpleFetch() to reduce copied code,
202                        //              but i dont know how yet. Ideas please!
203                        request = request || {};
204                        if(!request.store){
205                                request.store = this;
206                        }
207                        var self = this;
208               
209                        var _errorHandler = function(errorData, requestObject){
210                                if(requestObject.onError){
211                                        var scope = requestObject.scope || dojo.global;
212                                        requestObject.onError.call(scope, errorData, requestObject);
213                                }
214                        };
215               
216                        var _fetchHandler = function(items, requestObject, numRows){
217                                var oldAbortFunction = requestObject.abort || null;
218                                var aborted = false;
219                               
220                                var startIndex = requestObject.start?requestObject.start:0;
221                                if(self.doClientPaging == false){
222                                        // For client paging we dont need no slicing of the result.
223                                        startIndex = 0;
224                                }
225                                var endIndex = requestObject.count?(startIndex + requestObject.count):items.length;
226               
227                                requestObject.abort = function(){
228                                        aborted = true;
229                                        if(oldAbortFunction){
230                                                oldAbortFunction.call(requestObject);
231                                        }
232                                };
233               
234                                var scope = requestObject.scope || dojo.global;
235                                if(!requestObject.store){
236                                        requestObject.store = self;
237                                }
238                                if(requestObject.onBegin){
239                                        requestObject.onBegin.call(scope, numRows, requestObject);
240                                }
241                                if(requestObject.sort && self.doClientSorting){
242                                        items.sort(dojo.data.util.sorter.createSortFunction(requestObject.sort, self));
243                                }
244                                if(requestObject.onItem){
245                                        for(var i = startIndex; (i < items.length) && (i < endIndex); ++i){
246                                                var item = items[i];
247                                                if(!aborted){
248                                                        requestObject.onItem.call(scope, item, requestObject);
249                                                }
250                                        }
251                                }
252                                if(requestObject.onComplete && !aborted){
253                                        var subset = null;
254                                        if(!requestObject.onItem){
255                                                subset = items.slice(startIndex, endIndex);
256                                        }
257                                        requestObject.onComplete.call(scope, subset, requestObject);
258                                }
259                        };
260                        this._fetchItems(request, _fetchHandler, _errorHandler);
261                        return request; // Object
262                },
263       
264                getFeatures: function(){
265                        return this._features;
266                },
267       
268                close: function(/*dojo/data/api/Request|Object?*/ request){
269                        // I have no idea if this is really needed ...
270                },
271       
272                getLabel: function(/* item */ item){
273                        // summary:
274                        //              See dojo/data/api/Read.getLabel()
275                        if(this._labelAttr && this.isItem(item)){
276                                return this.getValue(item, this._labelAttr); //String
277                        }
278                        return undefined; //undefined
279                },
280       
281                getLabelAttributes: function(/* item */ item){
282                        // summary:
283                        //              See dojo/data/api/Read.getLabelAttributes()
284                        if(this._labelAttr){
285                                return [this._labelAttr]; //array
286                        }
287                        return null; //null
288                },
289               
290                _xhrFetchHandler: function(data, request, fetchHandler, errorHandler){
291                        data = this._filterResponse(data);
292                        if(data.label){
293                                this._labelAttr = data.label;
294                        }
295                        var numRows = data.numRows || -1;
296
297                        this._items = [];
298                        // Store a ref to "this" in each item, so we can simply check if an item
299                        // really origins form here (idea is from ItemFileReadStore, I just don't know
300                        // how efficient the real storage use, garbage collection effort, etc. is).
301                        dojo.forEach(data.items,function(e){
302                                this._items.push({i:e, r:this});
303                        },this);
304                       
305                        var identifier = data.identifier;
306                        this._itemsByIdentity = {};
307                        if(identifier){
308                                this._identifier = identifier;
309                                var i;
310                                for(i = 0; i < this._items.length; ++i){
311                                        var item = this._items[i].i;
312                                        var identity = item[identifier];
313                                        if(!this._itemsByIdentity[identity]){
314                                                this._itemsByIdentity[identity] = item;
315                                        }else{
316                                                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 + "]");
317                                        }
318                                }
319                        }else{
320                                this._identifier = Number;
321                                for(i = 0; i < this._items.length; ++i){
322                                        this._items[i].n = i;
323                                }
324                        }
325                       
326                        // TODO actually we should do the same as dojo.data.ItemFileReadStore._getItemsFromLoadedData() to sanitize
327                        // (does it really sanititze them) and store the data optimal. should we? for security reasons???
328                        numRows = this._numRows = (numRows === -1) ? this._items.length : numRows;
329                        fetchHandler(this._items, request, numRows);
330                        this._numRows = numRows;
331                },
332               
333                _fetchItems: function(request, fetchHandler, errorHandler){
334                        // summary:
335                        //              The request contains the data as defined in the Read-API.
336                        //              Additionally there is following keyword "serverQuery".
337                        //
338                        //              ####The *serverQuery* parameter, optional.
339                        //
340                        //              This parameter contains the data that will be sent to the server.
341                        //              If this parameter is not given the parameter "query"'s
342                        //              data are sent to the server. This is done for some reasons:
343                        //
344                        //              - to specify explicitly which data are sent to the server, they
345                        //                might also be a mix of what is contained in "query", "queryOptions"
346                        //                and the paging parameters "start" and "count" or may be even
347                        //                completely different things.
348                        //              - don't modify the request.query data, so the interface using this
349                        //                store can rely on unmodified data, as the combobox dijit currently
350                        //                does it, it compares if the query has changed
351                        //              - request.query is required by the Read-API
352                        //
353                        //              I.e. the following examples might be sent via GET:
354                        //      |         fetch({query:{name:"abc"}, queryOptions:{ignoreCase:true}})
355                        //                the URL will become:   /url.php?name=abc
356                        //
357                        //      |         fetch({serverQuery:{q:"abc", c:true}, query:{name:"abc"}, queryOptions:{ignoreCase:true}})
358                        //                the URL will become:   /url.php?q=abc&c=true
359                        //      |         // The serverQuery-parameter has overruled the query-parameter
360                        //      |         // but the query parameter stays untouched, but is not sent to the server!
361                        //      |         // The serverQuery contains more data than the query, so they might differ!
362
363                        var serverQuery = request.serverQuery || request.query || {};
364                        //Need to add start and count
365                        if(!this.doClientPaging){
366                                serverQuery.start = request.start || 0;
367                                // Count might not be sent if not given.
368                                if(request.count){
369                                        serverQuery.count = request.count;
370                                }
371                        }
372                        if(!this.doClientSorting && request.sort){
373                                var sortInfo = [];
374                                dojo.forEach(request.sort, function(sort){
375                                        if(sort && sort.attribute){
376                                                sortInfo.push((sort.descending ? "-" : "") + sort.attribute);
377                                        }
378                                });
379                                serverQuery.sort = sortInfo.join(',');
380                        }
381                        // Compare the last query and the current query by simply json-encoding them,
382                        // so we dont have to do any deep object compare ... is there some dojo.areObjectsEqual()???
383                        if(this.doClientPaging && this._lastServerQuery !== null &&
384                                dojo.toJson(serverQuery) == dojo.toJson(this._lastServerQuery)
385                                ){
386                                this._numRows = (this._numRows === -1) ? this._items.length : this._numRows;
387                                fetchHandler(this._items, request, this._numRows);
388                        }else{
389                                var xhrFunc = this.requestMethod.toLowerCase() == "post" ? dojo.xhrPost : dojo.xhrGet;
390                                var xhrHandler = xhrFunc({url:this.url, handleAs:"json-comment-optional", content:serverQuery, failOk: true});
391                                request.abort = function(){
392                                        xhrHandler.cancel();
393                                };
394                                xhrHandler.addCallback(dojo.hitch(this, function(data){
395                                        this._xhrFetchHandler(data, request, fetchHandler, errorHandler);
396                                }));
397                                xhrHandler.addErrback(function(error){
398                                        errorHandler(error, request);
399                                });
400                                // Generate the hash using the time in milliseconds and a randon number.
401                                // Since Math.randon() returns something like: 0.23453463, we just remove the "0."
402                                // probably just for esthetic reasons :-).
403                                this.lastRequestHash = new Date().getTime()+"-"+String(Math.random()).substring(2);
404                                this._lastServerQuery = dojo.mixin({}, serverQuery);
405                        }
406                },
407               
408                _filterResponse: function(data){
409                        // summary:
410                        //              If the data from servers needs to be processed before it can be processed by this
411                        //              store, then this function should be re-implemented in subclass. This default
412                        //              implementation just return the data unchanged.
413                        // data:
414                        //              The data received from server
415                        return data;
416                },
417       
418                _assertIsItem: function(/* item */ item){
419                        // summary:
420                        //              It throws an error if item is not valid, so you can call it in every method that needs to
421                        //              throw an error when item is invalid.
422                        // item:
423                        //              The item to test for being contained by the store.
424                        if(!this.isItem(item)){
425                                throw new Error(this._className+": Invalid item argument.");
426                        }
427                },
428       
429                _assertIsAttribute: function(/* attribute-name-string */ attribute){
430                        // summary:
431                        //              This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
432                        // attribute:
433                        //              The attribute to test for being contained by the store.
434                        if(typeof attribute !== "string"){
435                                throw new Error(this._className+": Invalid attribute argument ('"+attribute+"').");
436                        }
437                },
438       
439                fetchItemByIdentity: function(/* Object */ keywordArgs){
440                        // summary:
441                        //              See dojo/data/api/Identity.fetchItemByIdentity()
442       
443                        // See if we have already loaded the item with that id
444                        // In case there hasn't been a fetch yet, _itemsByIdentity is null
445                        // and thus a fetch will be triggered below.
446                        if(this._itemsByIdentity){
447                                var item = this._itemsByIdentity[keywordArgs.identity];
448                                if(!(item === undefined)){
449                                        if(keywordArgs.onItem){
450                                                var scope = keywordArgs.scope ? keywordArgs.scope : dojo.global;
451                                                keywordArgs.onItem.call(scope, {i:item, r:this});
452                                        }
453                                        return;
454                                }
455                        }
456       
457                        // Otherwise we need to go remote
458                        // Set up error handler
459                        var _errorHandler = function(errorData, requestObject){
460                                var scope = keywordArgs.scope ? keywordArgs.scope : dojo.global;
461                                if(keywordArgs.onError){
462                                        keywordArgs.onError.call(scope, errorData);
463                                }
464                        };
465                       
466                        // Set up fetch handler
467                        var _fetchHandler = function(items, requestObject){
468                                var scope = keywordArgs.scope ? keywordArgs.scope : dojo.global;
469                                try{
470                                        // There is supposed to be only one result
471                                        var item = null;
472                                        if(items && items.length == 1){
473                                                item = items[0];
474                                        }
475                                       
476                                        // If no item was found, item is still null and we'll
477                                        // fire the onItem event with the null here
478                                        if(keywordArgs.onItem){
479                                                keywordArgs.onItem.call(scope, item);
480                                        }
481                                }catch(error){
482                                        if(keywordArgs.onError){
483                                                keywordArgs.onError.call(scope, error);
484                                        }
485                                }
486                        };
487                       
488                        // Construct query
489                        var request = {serverQuery:{id:keywordArgs.identity}};
490                       
491                        // Dispatch query
492                        this._fetchItems(request, _fetchHandler, _errorHandler);
493                },
494               
495                getIdentity: function(/* item */ item){
496                        // summary:
497                        //              See dojo/data/api/Identity.getIdentity()
498                        var identifier = null;
499                        if(this._identifier === Number){
500                                identifier = item.n; // Number
501                        }else{
502                                identifier = item.i[this._identifier];
503                        }
504                        return identifier;
505                },
506               
507                getIdentityAttributes: function(/* item */ item){
508                        // summary:
509                        //              See dojo/data/api/Identity.getIdentityAttributes()
510                        return [this._identifier];
511                }
512        });
513
514});
Note: See TracBrowser for help on using the repository browser.