source: Dev/branches/rest-dojo-ui/client/dojox/data/AndOrReadStore.js @ 274

Last change on this file since 274 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).

  • Property svn:executable set to *
File size: 37.8 KB
Line 
1define(["dojo/_base/kernel", "dojo/_base/declare", "dojo/_base/lang", "dojo/data/util/filter", "dojo/data/util/simpleFetch",
2                "dojo/_base/array", "dojo/date/stamp", "dojo/_base/json", "dojo/_base/window", "dojo/_base/xhr"],
3  function(kernel, declare, lang, filterUtil, simpleFetch, array, dateStamp, json, winUtil, xhr) {
4
5var AndOrReadStore = declare("dojox.data.AndOrReadStore", null, {
6        //      summary:
7        //              AndOrReadStore uses ItemFileReadStore as a base, modifying only the query (_fetchItems) section.
8        //              Supports queries of the form: query:"id:1* OR dept:'Sales Department' || (id:2* && NOT dept:S*)"
9        //              Includes legacy/widget support via:
10        //                      query:{complexQuery:"id:1* OR dept:'Sales Department' || (id:2* && NOT dept:S*)"}
11        //              The ItemFileReadStore implements the dojo.data.api.Read API and reads
12        //              data from JSON files that have contents in this format --
13        //              { items: [
14        //                      { name:'Kermit', color:'green', age:12, friends:['Gonzo', {_reference:{name:'Fozzie Bear'}}]},
15        //                      { name:'Fozzie Bear', wears:['hat', 'tie']},
16        //                      { name:'Miss Piggy', pets:'Foo-Foo'}
17        //              ]}
18        //              Note that it can also contain an 'identifer' property that specified which attribute on the items
19        //              in the array of items that acts as the unique identifier for that item.
20        //
21        constructor: function(/* Object */ keywordParameters){
22                //      summary: constructor
23                //      keywordParameters: {url: String}
24                //      keywordParameters: {data: jsonObject}
25                //      keywordParameters: {typeMap: object)
26                //              The structure of the typeMap object is as follows:
27                //              {
28                //                      type0: function || object,
29                //                      type1: function || object,
30                //                      ...
31                //                      typeN: function || object
32                //              }
33                //              Where if it is a function, it is assumed to be an object constructor that takes the
34                //              value of _value as the initialization parameters.  If it is an object, then it is assumed
35                //              to be an object of general form:
36                //              {
37                //                      type: function, //constructor.
38                //                      deserialize:    function(value) //The function that parses the value and constructs the object defined by type appropriately.
39                //              }
40       
41                this._arrayOfAllItems = [];
42                this._arrayOfTopLevelItems = [];
43                this._loadFinished = false;
44                this._jsonFileUrl = keywordParameters.url;
45                this._ccUrl = keywordParameters.url;
46                this.url = keywordParameters.url;
47                this._jsonData = keywordParameters.data;
48                this.data = null;
49                this._datatypeMap = keywordParameters.typeMap || {};
50                if(!this._datatypeMap['Date']){
51                        //If no default mapping for dates, then set this as default.
52                        //We use the dojo.date.stamp here because the ISO format is the 'dojo way'
53                        //of generically representing dates.
54                        this._datatypeMap['Date'] = {
55                                                                                        type: Date,
56                                                                                        deserialize: function(value){
57                                                                                                return dateStamp.fromISOString(value);
58                                                                                        }
59                                                                                };
60                }
61                this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true};
62                this._itemsByIdentity = null;
63                this._storeRefPropName = "_S"; // Default name for the store reference to attach to every item.
64                this._itemNumPropName = "_0"; // Default Item Id for isItem to attach to every item.
65                this._rootItemPropName = "_RI"; // Default Item Id for isItem to attach to every item.
66                this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity
67                this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset.
68                this._queuedFetches = [];
69
70                if(keywordParameters.urlPreventCache !== undefined){
71                        this.urlPreventCache = keywordParameters.urlPreventCache?true:false;
72                }
73                if(keywordParameters.hierarchical !== undefined){
74                        this.hierarchical = keywordParameters.hierarchical?true:false;
75                }
76                if(keywordParameters.clearOnClose){
77                        this.clearOnClose = true;
78                }
79        },
80       
81        url: "", // use "" rather than undefined for the benefit of the parser (#3539)
82
83        //Internal var, crossCheckUrl.  Used so that setting either url or _jsonFileUrl, can still trigger a reload
84        //when clearOnClose and close is used.
85        _ccUrl: "",
86
87        data: null, //Make this parser settable.
88
89        typeMap: null, //Make this parser settable.
90
91        //Parameter to allow users to specify if a close call should force a reload or not.
92        //By default, it retains the old behavior of not clearing if close is called.  But
93        //if set true, the store will be reset to default state.  Note that by doing this,
94        //all item handles will become invalid and a new fetch must be issued.
95        clearOnClose: false,
96
97        //Parameter to allow specifying if preventCache should be passed to the xhrGet call or not when loading data from a url.
98        //Note this does not mean the store calls the server on each fetch, only that the data load has preventCache set as an option.
99        //Added for tracker: #6072
100        urlPreventCache: false,
101
102        //Parameter to indicate to process data from the url as hierarchical
103        //(data items can contain other data items in js form).  Default is true
104        //for backwards compatibility.  False means only root items are processed
105        //as items, all child objects outside of type-mapped objects and those in
106        //specific reference format, are left straight JS data objects.
107        hierarchical: true,
108
109        _assertIsItem: function(/* item */ item){
110                //      summary:
111                //              This function tests whether the item passed in is indeed an item in the store.
112                //      item:
113                //              The item to test for being contained by the store.
114                if(!this.isItem(item)){
115                        throw new Error("dojox.data.AndOrReadStore: Invalid item argument.");
116                }
117        },
118
119        _assertIsAttribute: function(/* attribute-name-string */ attribute){
120                //      summary:
121                //              This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
122                //      attribute:
123                //              The attribute to test for being contained by the store.
124                if(typeof attribute !== "string"){
125                        throw new Error("dojox.data.AndOrReadStore: Invalid attribute argument.");
126                }
127        },
128
129        getValue: function(     /* item */ item,
130                                                /* attribute-name-string */ attribute,
131                                                /* value? */ defaultValue){
132                //      summary:
133                //              See dojo.data.api.Read.getValue()
134                var values = this.getValues(item, attribute);
135                return (values.length > 0)?values[0]:defaultValue; // mixed
136        },
137
138        getValues: function(/* item */ item,
139                                                /* attribute-name-string */ attribute){
140                //      summary:
141                //              See dojo.data.api.Read.getValues()
142
143                this._assertIsItem(item);
144                this._assertIsAttribute(attribute);
145                var arr = item[attribute] || [];
146                // Clone it before returning.  refs: #10474
147                return arr.slice(0, arr.length); // Array
148        },
149
150        getAttributes: function(/* item */ item){
151                //      summary:
152                //              See dojo.data.api.Read.getAttributes()
153                this._assertIsItem(item);
154                var attributes = [];
155                for(var key in item){
156                        // Save off only the real item attributes, not the special id marks for O(1) isItem.
157                        if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){
158                                attributes.push(key);
159                        }
160                }
161                return attributes; // Array
162        },
163
164        hasAttribute: function( /* item */ item,
165                                                        /* attribute-name-string */ attribute){
166                //      summary:
167                //              See dojo.data.api.Read.hasAttribute()
168                this._assertIsItem(item);
169                this._assertIsAttribute(attribute);
170                return (attribute in item);
171        },
172
173        containsValue: function(/* item */ item,
174                                                        /* attribute-name-string */ attribute,
175                                                        /* anything */ value){
176                //      summary:
177                //              See dojo.data.api.Read.containsValue()
178                var regexp = undefined;
179                if(typeof value === "string"){
180                        regexp = filterUtil.patternToRegExp(value, false);
181                }
182                return this._containsValue(item, attribute, value, regexp); //boolean.
183        },
184
185        _containsValue: function(       /* item */ item,
186                                                                /* attribute-name-string */ attribute,
187                                                                /* anything */ value,
188                                                                /* RegExp?*/ regexp){
189                //      summary:
190                //              Internal function for looking at the values contained by the item.
191                //      description:
192                //              Internal function for looking at the values contained by the item.  This
193                //              function allows for denoting if the comparison should be case sensitive for
194                //              strings or not (for handling filtering cases where string case should not matter)
195                //
196                //      item:
197                //              The data item to examine for attribute values.
198                //      attribute:
199                //              The attribute to inspect.
200                //      value:
201                //              The value to match.
202                //      regexp:
203                //              Optional regular expression generated off value if value was of string type to handle wildcarding.
204                //              If present and attribute values are string, then it can be used for comparison instead of 'value'
205                return array.some(this.getValues(item, attribute), function(possibleValue){
206                        if(possibleValue !== null && !lang.isObject(possibleValue) && regexp){
207                                if(possibleValue.toString().match(regexp)){
208                                        return true; // Boolean
209                                }
210                        } else if(value === possibleValue){
211                                return true; // Boolean
212                        } else {
213                                return false;
214                        }
215                });
216        },
217
218        isItem: function(/* anything */ something){
219                //      summary:
220                //              See dojo.data.api.Read.isItem()
221                if(something && something[this._storeRefPropName] === this){
222                        if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){
223                                return true;
224                        }
225                }
226                return false; // Boolean
227        },
228
229        isItemLoaded: function(/* anything */ something){
230                //      summary:
231                //              See dojo.data.api.Read.isItemLoaded()
232                return this.isItem(something); //boolean
233        },
234
235        loadItem: function(/* object */ keywordArgs){
236                //      summary:
237                //              See dojo.data.api.Read.loadItem()
238                this._assertIsItem(keywordArgs.item);
239        },
240
241        getFeatures: function(){
242                //      summary:
243                //              See dojo.data.api.Read.getFeatures()
244                return this._features; //Object
245        },
246
247        getLabel: function(/* item */ item){
248                //      summary:
249                //              See dojo.data.api.Read.getLabel()
250                if(this._labelAttr && this.isItem(item)){
251                        return this.getValue(item,this._labelAttr); //String
252                }
253                return undefined; //undefined
254        },
255
256        getLabelAttributes: function(/* item */ item){
257                //      summary:
258                //              See dojo.data.api.Read.getLabelAttributes()
259                if(this._labelAttr){
260                        return [this._labelAttr]; //array
261                }
262                return null; //null
263        },
264
265        _fetchItems: function(  /* Object */ keywordArgs,
266                                                        /* Function */ findCallback,
267                                                        /* Function */ errorCallback){
268                //      summary:
269                //              See dojo.data.util.simpleFetch.fetch()
270                //              filter modified to permit complex queries where
271                //                      logical operators are case insensitive:
272                //                      , NOT AND OR ( ) ! && ||
273                //                      Note:  "," included for quoted/string legacy queries.
274                var self = this;
275                var filter = function(requestArgs, arrayOfItems){
276                        var items = [];
277                        if(requestArgs.query){
278                                //Complete copy, we may have to mess with it.
279                                //Safer than clone, which does a shallow copy, I believe.
280                                var query = json.fromJson(json.toJson(requestArgs.query));
281                                //Okay, object form query, we have to check to see if someone mixed query methods (such as using FilteringSelect
282                                //with a complexQuery).  In that case, the params need to be anded to the complex query statement.
283                                //See defect #7980
284                                if(typeof query == "object" ){
285                                        var count = 0;
286                                        var p;
287                                        for(p in query){
288                                                count++;
289                                        }
290                                        if(count > 1 && query.complexQuery){
291                                                var cq = query.complexQuery;
292                                                var wrapped = false;
293                                                for(p in query){
294                                                        if(p !== "complexQuery"){
295                                                                //We should wrap this in () as it should and with the entire complex query
296                                                                //Not just part of it.
297                                                                if(!wrapped){
298                                                                        cq = "( " + cq + " )";
299                                                                        wrapped = true;
300                                                                }
301                                                                //Make sure strings are quoted when going into complexQuery merge.
302                                                                var v = requestArgs.query[p];
303                                                                if(lang.isString(v)){
304                                                                        v = "'" + v + "'";
305                                                                }
306                                                                cq += " AND " + p + ":" + v;
307                                                                delete query[p];
308                                                               
309                                                        }
310                                                }
311                                                query.complexQuery = cq;
312                                        }
313                                }
314
315                                var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false;
316                                //for complex queries only:  pattern = query[:|=]"NOT id:23* AND (type:'test*' OR dept:'bob') && !filed:true"
317                                //logical operators are case insensitive:  , NOT AND OR ( ) ! && ||  // "," included for quoted/string legacy queries.
318                                if(typeof query != "string"){
319                                        query = json.toJson(query);
320                                        query = query.replace(/\\\\/g,"\\"); //counter toJson expansion of backslashes, e.g., foo\\*bar test.
321                                }
322                                query = query.replace(/\\"/g,"\"");   //ditto, for embedded \" in lieu of " availability.
323                                var complexQuery = lang.trim(query.replace(/{|}/g,"")); //we can handle these, too.
324                                var pos2, i;
325                                if(complexQuery.match(/"? *complexQuery *"?:/)){ //case where widget required a json object, so use complexQuery:'the real query'
326                                        complexQuery = lang.trim(complexQuery.replace(/"?\s*complexQuery\s*"?:/,""));
327                                        var quotes = ["'",'"'];
328                                        var pos1,colon;
329                                        var flag = false;
330                                        for(i = 0; i<quotes.length; i++){
331                                                pos1 = complexQuery.indexOf(quotes[i]);
332                                                pos2 = complexQuery.indexOf(quotes[i],1);
333                                                colon = complexQuery.indexOf(":",1);
334                                                if(pos1 === 0 && pos2 != -1 && colon < pos2){
335                                                        flag = true;
336                                                        break;
337                                                } //first two sets of quotes don't occur before the first colon.
338                                        }
339                                        if(flag){       //dojo.toJson, and maybe user, adds surrounding quotes, which we need to remove.
340                                                complexQuery = complexQuery.replace(/^\"|^\'|\"$|\'$/g,"");
341                                        }
342                                } //end query="{complexQuery:'id:1* || dept:Sales'}" parsing (for when widget required json object query).
343                                var complexQuerySave = complexQuery;
344                                //valid logical operators.
345                                var begRegExp = /^,|^NOT |^AND |^OR |^\(|^\)|^!|^&&|^\|\|/i; //trailing space on some tokens on purpose.
346                                var sQuery = ""; //will be eval'ed for each i-th candidateItem, based on query components.
347                                var op = "";
348                                var val = "";
349                                var pos = -1;
350                                var err = false;
351                                var key = "";
352                                var value = "";
353                                var tok = "";
354                                pos2 = -1;
355                                for(i = 0; i < arrayOfItems.length; ++i){
356                                        var match = true;
357                                        var candidateItem = arrayOfItems[i];
358                                        if(candidateItem === null){
359                                                match = false;
360                                        }else{
361                                                //process entire string for this i-th candidateItem.
362                                                complexQuery = complexQuerySave; //restore query for next candidateItem.
363                                                sQuery = "";
364                                                //work left to right, finding either key:value pair or logical operator at the beginning of the complexQuery string.
365                                                //when found, concatenate to sQuery and remove from complexQuery and loop back.
366                                                while(complexQuery.length > 0 && !err){
367                                                        op = complexQuery.match(begRegExp);
368                                                       
369                                                        //get/process/append one or two leading logical operators.
370                                                        while(op && !err){ //look for leading logical operators.
371                                                                complexQuery = lang.trim(complexQuery.replace(op[0],""));
372                                                                op = lang.trim(op[0]).toUpperCase();
373                                                                //convert some logical operators to their javascript equivalents for later eval.
374                                                                op = op == "NOT" ? "!" : op == "AND" || op == "," ? "&&" : op == "OR" ? "||" : op;
375                                                                op = " " + op + " ";
376                                                                sQuery += op;
377                                                                op = complexQuery.match(begRegExp);
378                                                        }//end op && !err
379                                                       
380                                                        //now get/process/append one key:value pair.
381                                                        if(complexQuery.length > 0){
382                                                                pos = complexQuery.indexOf(":");
383                                                                if(pos == -1){
384                                                                        err = true;
385                                                                        break;
386                                                                }else{
387                                                                        key = lang.trim(complexQuery.substring(0,pos).replace(/\"|\'/g,""));
388                                                                        complexQuery = lang.trim(complexQuery.substring(pos + 1));
389                                                                        tok = complexQuery.match(/^\'|^\"/);    //quoted?
390                                                                        if(tok){
391                                                                                tok = tok[0];
392                                                                                pos = complexQuery.indexOf(tok);
393                                                                                pos2 = complexQuery.indexOf(tok,pos + 1);
394                                                                                if(pos2 == -1){
395                                                                                        err = true;
396                                                                                        break;
397                                                                                }
398                                                                                value = complexQuery.substring(pos + 1,pos2);
399                                                                                if(pos2 == complexQuery.length - 1){ //quote is last character
400                                                                                        complexQuery = "";
401                                                                                }else{
402                                                                                        complexQuery = lang.trim(complexQuery.substring(pos2 + 1));
403                                                                                }
404                                                                                sQuery += self._containsValue(candidateItem, key, value, filterUtil.patternToRegExp(value, ignoreCase));
405                                                                        }
406                                                                        else{ //not quoted, so a space, comma, or closing parens (or the end) will be the break.
407                                                                                tok = complexQuery.match(/\s|\)|,/);
408                                                                                if(tok){
409                                                                                        var pos3 = new Array(tok.length);
410                                                                                        for(var j = 0;j<tok.length;j++){
411                                                                                                pos3[j] = complexQuery.indexOf(tok[j]);
412                                                                                        }
413                                                                                        pos = pos3[0];
414                                                                                        if(pos3.length > 1){
415                                                                                                for(var j=1;j<pos3.length;j++){
416                                                                                                        pos = Math.min(pos,pos3[j]);
417                                                                                                }
418                                                                                        }
419                                                                                        value = lang.trim(complexQuery.substring(0,pos));
420                                                                                        complexQuery = lang.trim(complexQuery.substring(pos));
421                                                                                }else{ //not a space, so must be at the end of the complexQuery.
422                                                                                        value = lang.trim(complexQuery);
423                                                                                        complexQuery = "";
424                                                                                } //end  inner if(tok) else
425                                                                                sQuery += self._containsValue(candidateItem, key, value, filterUtil.patternToRegExp(value, ignoreCase));
426                                                                        } //end outer if(tok) else
427                                                                } //end found ":"
428                                                        } //end if(complexQuery.length > 0)
429                                                } //end while complexQuery.length > 0 && !err, so finished the i-th item.
430                                                match = eval(sQuery);
431                                        } //end else is non-null candidateItem.
432                                        if(match){
433                                                items.push(candidateItem);
434                                        }
435                                } //end for/next of all items.
436                                if(err){
437                                        //soft fail.
438                                        items = [];
439                                        console.log("The store's _fetchItems failed, probably due to a syntax error in query.");
440                                }
441                                findCallback(items, requestArgs);
442                        }else{
443                                // No query...
444                                // We want a copy to pass back in case the parent wishes to sort the array.
445                                // We shouldn't allow resort of the internal list, so that multiple callers
446                                // can get lists and sort without affecting each other.  We also need to
447                                // filter out any null values that have been left as a result of deleteItem()
448                                // calls in ItemFileWriteStore.
449                                for(var i = 0; i < arrayOfItems.length; ++i){
450                                        var item = arrayOfItems[i];
451                                        if(item !== null){
452                                                items.push(item);
453                                        }
454                                }
455                                findCallback(items, requestArgs);
456                        } //end if there is a query.
457                }; //end filter function
458
459                if(this._loadFinished){
460                        filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
461                }else{
462                        if(this._jsonFileUrl !== this._ccUrl){
463                                kernel.deprecated("dojox.data.AndOrReadStore: ",
464                                                                "To change the url, set the url property of the store," +
465                                                                " not _jsonFileUrl.  _jsonFileUrl support will be removed in 2.0");
466                                this._ccUrl = this._jsonFileUrl;
467                                this.url = this._jsonFileUrl;
468                        }else if(this.url !== this._ccUrl){
469                                this._jsonFileUrl = this.url;
470                                this._ccUrl = this.url;
471                        }
472                        //See if there was any forced reset of data.
473                        if(this.data != null && this._jsonData == null){
474                                this._jsonData = this.data;
475                                this.data = null;
476                        }
477                        if(this._jsonFileUrl){
478                                //If fetches come in before the loading has finished, but while
479                                //a load is in progress, we have to defer the fetching to be
480                                //invoked in the callback.
481                                if(this._loadInProgress){
482                                        this._queuedFetches.push({args: keywordArgs, filter: filter});
483                                }else{
484                                        this._loadInProgress = true;
485                                        var getArgs = {
486                                                        url: self._jsonFileUrl,
487                                                        handleAs: "json-comment-optional",
488                                                        preventCache: this.urlPreventCache
489                                                };
490                                        var getHandler = xhr.get(getArgs);
491                                        getHandler.addCallback(function(data){
492                                                try{
493                                                        self._getItemsFromLoadedData(data);
494                                                        self._loadFinished = true;
495                                                        self._loadInProgress = false;
496                                                       
497                                                        filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions));
498                                                        self._handleQueuedFetches();
499                                                }catch(e){
500                                                        self._loadFinished = true;
501                                                        self._loadInProgress = false;
502                                                        errorCallback(e, keywordArgs);
503                                                }
504                                        });
505                                        getHandler.addErrback(function(error){
506                                                self._loadInProgress = false;
507                                                errorCallback(error, keywordArgs);
508                                        });
509                                       
510                                        //Wire up the cancel to abort of the request
511                                        //This call cancel on the deferred if it hasn't been called
512                                        //yet and then will chain to the simple abort of the
513                                        //simpleFetch keywordArgs
514                                        var oldAbort = null;
515                                        if(keywordArgs.abort){
516                                                oldAbort = keywordArgs.abort;
517                                        }
518                                        keywordArgs.abort = function(){
519                                                var df = getHandler;
520                                                if(df && df.fired === -1){
521                                                        df.cancel();
522                                                        df = null;
523                                                }
524                                                if(oldAbort){
525                                                        oldAbort.call(keywordArgs);
526                                                }
527                                        };
528                                }
529                        }else if(this._jsonData){
530                                try{
531                                        this._loadFinished = true;
532                                        this._getItemsFromLoadedData(this._jsonData);
533                                        this._jsonData = null;
534                                        filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
535                                }catch(e){
536                                        errorCallback(e, keywordArgs);
537                                }
538                        }else{
539                                errorCallback(new Error("dojox.data.AndOrReadStore: No JSON source data was provided as either URL or a nested Javascript object."), keywordArgs);
540                        }
541                } //end deferred fetching.
542        }, //end _fetchItems
543
544        _handleQueuedFetches: function(){
545                //      summary:
546                //              Internal function to execute delayed request in the store.
547                //Execute any deferred fetches now.
548                if(this._queuedFetches.length > 0){
549                        for(var i = 0; i < this._queuedFetches.length; i++){
550                                var fData = this._queuedFetches[i];
551                                var delayedQuery = fData.args;
552                                var delayedFilter = fData.filter;
553                                if(delayedFilter){
554                                        delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions));
555                                }else{
556                                        this.fetchItemByIdentity(delayedQuery);
557                                }
558                        }
559                        this._queuedFetches = [];
560                }
561        },
562
563        _getItemsArray: function(/*object?*/queryOptions){
564                //      summary:
565                //              Internal function to determine which list of items to search over.
566                //      queryOptions: The query options parameter, if any.
567                if(queryOptions && queryOptions.deep){
568                        return this._arrayOfAllItems;
569                }
570                return this._arrayOfTopLevelItems;
571        },
572
573        close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
574                //      summary:
575                //              See dojo.data.api.Read.close()
576                if(this.clearOnClose &&
577                        this._loadFinished &&
578                        !this._loadInProgress){
579                         //Reset all internalsback to default state.  This will force a reload
580                         //on next fetch.  This also checks that the data or url param was set
581                         //so that the store knows it can get data.  Without one of those being set,
582                         //the next fetch will trigger an error.
583
584                         if(((this._jsonFileUrl == "" || this._jsonFileUrl == null) &&
585                                 (this.url == "" || this.url == null)
586                                ) && this.data == null){
587                                 console.debug("dojox.data.AndOrReadStore: WARNING!  Data reload " +
588                                        " information has not been provided." +
589                                        "  Please set 'url' or 'data' to the appropriate value before" +
590                                        " the next fetch");
591                         }
592                         this._arrayOfAllItems = [];
593                         this._arrayOfTopLevelItems = [];
594                         this._loadFinished = false;
595                         this._itemsByIdentity = null;
596                         this._loadInProgress = false;
597                         this._queuedFetches = [];
598                 }
599        },
600
601        _getItemsFromLoadedData: function(/* Object */ dataObject){
602                //      summary:
603                //              Function to parse the loaded data into item format and build the internal items array.
604                //      description:
605                //              Function to parse the loaded data into item format and build the internal items array.
606                //
607                //      dataObject:
608                //              The JS data object containing the raw data to convery into item format.
609                //
610                //      returns: array
611                //              Array of items in store item format.
612               
613                // First, we define a couple little utility functions...
614               
615                var self = this;
616                function valueIsAnItem(/* anything */ aValue){
617                        // summary:
618                        //              Given any sort of value that could be in the raw json data,
619                        //              return true if we should interpret the value as being an
620                        //              item itself, rather than a literal value or a reference.
621                        // example:
622                        //      |       false == valueIsAnItem("Kermit");
623                        //      |       false == valueIsAnItem(42);
624                        //      |       false == valueIsAnItem(new Date());
625                        //      |       false == valueIsAnItem({_type:'Date', _value:'May 14, 1802'});
626                        //      |       false == valueIsAnItem({_reference:'Kermit'});
627                        //      |       true == valueIsAnItem({name:'Kermit', color:'green'});
628                        //      |       true == valueIsAnItem({iggy:'pop'});
629                        //      |       true == valueIsAnItem({foo:42});
630                        var isItem = (
631                                (aValue !== null) &&
632                                (typeof aValue === "object") &&
633                                (!lang.isArray(aValue)) &&
634                                (!lang.isFunction(aValue)) &&
635                                (aValue.constructor == Object) &&
636                                (typeof aValue._reference === "undefined") &&
637                                (typeof aValue._type === "undefined") &&
638                                (typeof aValue._value === "undefined") &&
639                                self.hierarchical
640                        );
641                        return isItem;
642                }
643               
644                function addItemAndSubItemsToArrayOfAllItems(/* Item */ anItem){
645                        self._arrayOfAllItems.push(anItem);
646                        for(var attribute in anItem){
647                                var valueForAttribute = anItem[attribute];
648                                if(valueForAttribute){
649                                        if(lang.isArray(valueForAttribute)){
650                                                var valueArray = valueForAttribute;
651                                                for(var k = 0; k < valueArray.length; ++k){
652                                                        var singleValue = valueArray[k];
653                                                        if(valueIsAnItem(singleValue)){
654                                                                addItemAndSubItemsToArrayOfAllItems(singleValue);
655                                                        }
656                                                }
657                                        }else{
658                                                if(valueIsAnItem(valueForAttribute)){
659                                                        addItemAndSubItemsToArrayOfAllItems(valueForAttribute);
660                                                }
661                                        }
662                                }
663                        }
664                }
665
666                this._labelAttr = dataObject.label;
667
668                // We need to do some transformations to convert the data structure
669                // that we read from the file into a format that will be convenient
670                // to work with in memory.
671
672                // Step 1: Walk through the object hierarchy and build a list of all items
673                var i;
674                var item;
675                this._arrayOfAllItems = [];
676                this._arrayOfTopLevelItems = dataObject.items;
677
678                for(i = 0; i < this._arrayOfTopLevelItems.length; ++i){
679                        item = this._arrayOfTopLevelItems[i];
680                        addItemAndSubItemsToArrayOfAllItems(item);
681                        item[this._rootItemPropName]=true;
682                }
683
684                // Step 2: Walk through all the attribute values of all the items,
685                // and replace single values with arrays.  For example, we change this:
686                //              { name:'Miss Piggy', pets:'Foo-Foo'}
687                // into this:
688                //              { name:['Miss Piggy'], pets:['Foo-Foo']}
689                //
690                // We also store the attribute names so we can validate our store
691                // reference and item id special properties for the O(1) isItem
692                var allAttributeNames = {};
693                var key;
694
695                for(i = 0; i < this._arrayOfAllItems.length; ++i){
696                        item = this._arrayOfAllItems[i];
697                        for(key in item){
698                                if(key !== this._rootItemPropName){
699                                        var value = item[key];
700                                        if(value !== null){
701                                                if(!lang.isArray(value)){
702                                                        item[key] = [value];
703                                                }
704                                        }else{
705                                                item[key] = [null];
706                                        }
707                                }
708                                allAttributeNames[key]=key;
709                        }
710                }
711
712                // Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName
713                // This should go really fast, it will generally never even run the loop.
714                while(allAttributeNames[this._storeRefPropName]){
715                        this._storeRefPropName += "_";
716                }
717                while(allAttributeNames[this._itemNumPropName]){
718                        this._itemNumPropName += "_";
719                }
720                while(allAttributeNames[this._reverseRefMap]){
721                        this._reverseRefMap += "_";
722                }
723
724                // Step 4: Some data files specify an optional 'identifier', which is
725                // the name of an attribute that holds the identity of each item.
726                // If this data file specified an identifier attribute, then build a
727                // hash table of items keyed by the identity of the items.
728                var arrayOfValues;
729
730                var identifier = dataObject.identifier;
731                if(identifier){
732                        this._itemsByIdentity = {};
733                        this._features['dojo.data.api.Identity'] = identifier;
734                        for(i = 0; i < this._arrayOfAllItems.length; ++i){
735                                item = this._arrayOfAllItems[i];
736                                arrayOfValues = item[identifier];
737                                var identity = arrayOfValues[0];
738                                if(!this._itemsByIdentity[identity]){
739                                        this._itemsByIdentity[identity] = item;
740                                }else{
741                                        if(this._jsonFileUrl){
742                                                throw new Error("dojox.data.AndOrReadStore:  The json data as specified by: [" + this._jsonFileUrl + "] is malformed.  Items within the list have identifier: [" + identifier + "].  Value collided: [" + identity + "]");
743                                        }else if(this._jsonData){
744                                                throw new Error("dojox.data.AndOrReadStore:  The json data provided by the creation arguments is malformed.  Items within the list have identifier: [" + identifier + "].  Value collided: [" + identity + "]");
745                                        }
746                                }
747                        }
748                }else{
749                        this._features['dojo.data.api.Identity'] = Number;
750                }
751
752                // Step 5: Walk through all the items, and set each item's properties
753                // for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true.
754                for(i = 0; i < this._arrayOfAllItems.length; ++i){
755                        item = this._arrayOfAllItems[i];
756                        item[this._storeRefPropName] = this;
757                        item[this._itemNumPropName] = i;
758                }
759
760                // Step 6: We walk through all the attribute values of all the items,
761                // looking for type/value literals and item-references.
762                //
763                // We replace item-references with pointers to items.  For example, we change:
764                //              { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
765                // into this:
766                //              { name:['Kermit'], friends:[miss_piggy] }
767                // (where miss_piggy is the object representing the 'Miss Piggy' item).
768                //
769                // We replace type/value pairs with typed-literals.  For example, we change:
770                //              { name:['Nelson Mandela'], born:[{_type:'Date', _value:'July 18, 1918'}] }
771                // into this:
772                //              { name:['Kermit'], born:(new Date('July 18, 1918')) }
773                //
774                // We also generate the associate map for all items for the O(1) isItem function.
775                for(i = 0; i < this._arrayOfAllItems.length; ++i){
776                        item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
777                        for(key in item){
778                                arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}]
779                                for(var j = 0; j < arrayOfValues.length; ++j){
780                                        value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}}
781                                        if(value !== null && typeof value == "object"){
782                                                if(("_type" in value) && ("_value" in value)){
783                                                        var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber'
784                                                        var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}}
785                                                        if(!mappingObj){
786                                                                throw new Error("dojox.data.AndOrReadStore: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'");
787                                                        }else if(lang.isFunction(mappingObj)){
788                                                                arrayOfValues[j] = new mappingObj(value._value);
789                                                        }else if(lang.isFunction(mappingObj.deserialize)){
790                                                                arrayOfValues[j] = mappingObj.deserialize(value._value);
791                                                        }else{
792                                                                throw new Error("dojox.data.AndOrReadStore: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function");
793                                                        }
794                                                }
795                                                if(value._reference){
796                                                        var referenceDescription = value._reference; // example: {name:'Miss Piggy'}
797                                                        if(!lang.isObject(referenceDescription)){
798                                                                // example: 'Miss Piggy'
799                                                                // from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]}
800                                                                arrayOfValues[j] = this._getItemByIdentity(referenceDescription);
801                                                        }else{
802                                                                // example: {name:'Miss Piggy'}
803                                                                // from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
804                                                                for(var k = 0; k < this._arrayOfAllItems.length; ++k){
805                                                                        var candidateItem = this._arrayOfAllItems[k];
806                                                                        var found = true;
807                                                                        for(var refKey in referenceDescription){
808                                                                                if(candidateItem[refKey] != referenceDescription[refKey]){
809                                                                                        found = false;
810                                                                                }
811                                                                        }
812                                                                        if(found){
813                                                                                arrayOfValues[j] = candidateItem;
814                                                                        }
815                                                                }
816                                                        }
817                                                        if(this.referenceIntegrity){
818                                                                var refItem = arrayOfValues[j];
819                                                                if(this.isItem(refItem)){
820                                                                        this._addReferenceToMap(refItem, item, key);
821                                                                }
822                                                        }
823                                                }else if(this.isItem(value)){
824                                                        //It's a child item (not one referenced through _reference).
825                                                        //We need to treat this as a referenced item, so it can be cleaned up
826                                                        //in a write store easily.
827                                                        if(this.referenceIntegrity){
828                                                                this._addReferenceToMap(value, item, key);
829                                                        }
830                                                }
831                                        }
832                                }
833                        }
834                }
835        },
836
837        _addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){
838                 //     summary:
839                 //             Method to add an reference map entry for an item and attribute.
840                 //     description:
841                 //             Method to add an reference map entry for an item and attribute.                  //
842                 //     refItem:
843                 //             The item that is referenced.
844                 //     parentItem:
845                 //             The item that holds the new reference to refItem.
846                 //     attribute:
847                 //             The attribute on parentItem that contains the new reference.
848                 
849                 //Stub function, does nothing.  Real processing is in ItemFileWriteStore.
850        },
851
852        getIdentity: function(/* item */ item){
853                //      summary:
854                //              See dojo.data.api.Identity.getIdentity()
855                var identifier = this._features['dojo.data.api.Identity'];
856                if(identifier === Number){
857                        return item[this._itemNumPropName]; // Number
858                }else{
859                        var arrayOfValues = item[identifier];
860                        if(arrayOfValues){
861                                return arrayOfValues[0]; // Object || String
862                        }
863                }
864                return null; // null
865        },
866
867        fetchItemByIdentity: function(/* Object */ keywordArgs){
868                //      summary:
869                //              See dojo.data.api.Identity.fetchItemByIdentity()
870
871                // Hasn't loaded yet, we have to trigger the load.
872                if(!this._loadFinished){
873                        var self = this;
874                        if(this._jsonFileUrl !== this._ccUrl){
875                                kernel.deprecated("dojox.data.AndOrReadStore: ",
876                                                                "To change the url, set the url property of the store," +
877                                                                " not _jsonFileUrl.  _jsonFileUrl support will be removed in 2.0");
878                                this._ccUrl = this._jsonFileUrl;
879                                this.url = this._jsonFileUrl;
880                        }else if(this.url !== this._ccUrl){
881                                this._jsonFileUrl = this.url;
882                                this._ccUrl = this.url;
883                        }
884                        //See if there was any forced reset of data.
885                        if(this.data != null && this._jsonData == null){
886                                this._jsonData = this.data;
887                                this.data = null;
888                        }
889                        if(this._jsonFileUrl){
890
891                                if(this._loadInProgress){
892                                        this._queuedFetches.push({args: keywordArgs});
893                                }else{
894                                        this._loadInProgress = true;
895                                        var getArgs = {
896                                                        url: self._jsonFileUrl,
897                                                        handleAs: "json-comment-optional",
898                                                        preventCache: this.urlPreventCache
899                                        };
900                                        var getHandler = xhr.get(getArgs);
901                                        getHandler.addCallback(function(data){
902                                                var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
903                                                try{
904                                                        self._getItemsFromLoadedData(data);
905                                                        self._loadFinished = true;
906                                                        self._loadInProgress = false;
907                                                        var item = self._getItemByIdentity(keywordArgs.identity);
908                                                        if(keywordArgs.onItem){
909                                                                keywordArgs.onItem.call(scope, item);
910                                                        }
911                                                        self._handleQueuedFetches();
912                                                }catch(error){
913                                                        self._loadInProgress = false;
914                                                        if(keywordArgs.onError){
915                                                                keywordArgs.onError.call(scope, error);
916                                                        }
917                                                }
918                                        });
919                                        getHandler.addErrback(function(error){
920                                                self._loadInProgress = false;
921                                                if(keywordArgs.onError){
922                                                        var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
923                                                        keywordArgs.onError.call(scope, error);
924                                                }
925                                        });
926                                }
927
928                        }else if(this._jsonData){
929                                // Passed in data, no need to xhr.
930                                self._getItemsFromLoadedData(self._jsonData);
931                                self._jsonData = null;
932                                self._loadFinished = true;
933                                var item = self._getItemByIdentity(keywordArgs.identity);
934                                if(keywordArgs.onItem){
935                                        var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
936                                        keywordArgs.onItem.call(scope, item);
937                                }
938                        }
939                }else{
940                        // Already loaded.  We can just look it up and call back.
941                        var item = this._getItemByIdentity(keywordArgs.identity);
942                        if(keywordArgs.onItem){
943                                var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
944                                keywordArgs.onItem.call(scope, item);
945                        }
946                }
947        },
948
949        _getItemByIdentity: function(/* Object */ identity){
950                //      summary:
951                //              Internal function to look an item up by its identity map.
952                var item = null;
953                if(this._itemsByIdentity){
954                        item = this._itemsByIdentity[identity];
955                }else{
956                        item = this._arrayOfAllItems[identity];
957                }
958                if(item === undefined){
959                        item = null;
960                }
961                return item; // Object
962        },
963
964        getIdentityAttributes: function(/* item */ item){
965                //      summary:
966                //              See dojo.data.api.Identity.getIdentifierAttributes()
967                 
968                var identifier = this._features['dojo.data.api.Identity'];
969                if(identifier === Number){
970                        // If (identifier === Number) it means getIdentity() just returns
971                        // an integer item-number for each item.  The dojo.data.api.Identity
972                        // spec says we need to return null if the identity is not composed
973                        // of attributes
974                        return null; // null
975                }else{
976                        return [identifier]; // Array
977                }
978        },
979       
980        _forceLoad: function(){
981                //      summary:
982                //              Internal function to force a load of the store if it hasn't occurred yet.  This is required
983                //              for specific functions to work properly.
984                var self = this;
985                if(this._jsonFileUrl !== this._ccUrl){
986                        kernel.deprecated("dojox.data.AndOrReadStore: ",
987                                                        "To change the url, set the url property of the store," +
988                                                        " not _jsonFileUrl.  _jsonFileUrl support will be removed in 2.0");
989                        this._ccUrl = this._jsonFileUrl;
990                        this.url = this._jsonFileUrl;
991                }else if(this.url !== this._ccUrl){
992                        this._jsonFileUrl = this.url;
993                        this._ccUrl = this.url;
994                }
995                //See if there was any forced reset of data.
996                if(this.data != null && this._jsonData == null){
997                        this._jsonData = this.data;
998                        this.data = null;
999                }
1000                if(this._jsonFileUrl){
1001                                var getArgs = {
1002                                        url: self._jsonFileUrl,
1003                                        handleAs: "json-comment-optional",
1004                                        preventCache: this.urlPreventCache,
1005                                        sync: true
1006                                };
1007                        var getHandler = xhr.get(getArgs);
1008                        getHandler.addCallback(function(data){
1009                                try{
1010                                        //Check to be sure there wasn't another load going on concurrently
1011                                        //So we don't clobber data that comes in on it.  If there is a load going on
1012                                        //then do not save this data.  It will potentially clobber current data.
1013                                        //We mainly wanted to sync/wait here.
1014                                        //TODO:  Revisit the loading scheme of this store to improve multi-initial
1015                                        //request handling.
1016                                        if(self._loadInProgress !== true && !self._loadFinished){
1017                                                self._getItemsFromLoadedData(data);
1018                                                self._loadFinished = true;
1019                                        }else if(self._loadInProgress){
1020                                                //Okay, we hit an error state we can't recover from.  A forced load occurred
1021                                                //while an async load was occurring.  Since we cannot block at this point, the best
1022                                                //that can be managed is to throw an error.
1023                                                throw new Error("dojox.data.AndOrReadStore:  Unable to perform a synchronous load, an async load is in progress.");
1024                                        }
1025                                }catch(e){
1026                                        console.log(e);
1027                                        throw e;
1028                                }
1029                        });
1030                        getHandler.addErrback(function(error){
1031                                throw error;
1032                        });
1033                }else if(this._jsonData){
1034                        self._getItemsFromLoadedData(self._jsonData);
1035                        self._jsonData = null;
1036                        self._loadFinished = true;
1037                }
1038        }
1039});
1040//Mix in the simple fetch implementation to this class.
1041lang.extend(AndOrReadStore, simpleFetch);
1042
1043return AndOrReadStore;
1044});
1045
1046
Note: See TracBrowser for help on using the repository browser.