source: Dev/branches/rest-dojo-ui/client/dojox/data/QueryReadStore.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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