]> git.pond.sub.org Git - eow/blob - static/dojo-release-1.1.1/dojox/data/CsvStore.js
add Dojo 1.1.1
[eow] / static / dojo-release-1.1.1 / dojox / data / CsvStore.js
1 if(!dojo._hasResource["dojox.data.CsvStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2 dojo._hasResource["dojox.data.CsvStore"] = true;
3 dojo.provide("dojox.data.CsvStore");
4
5 dojo.require("dojo.data.util.filter");
6 dojo.require("dojo.data.util.simpleFetch");
7
8 dojo.declare("dojox.data.CsvStore", null, {
9         //      summary:
10         //              The CsvStore implements the dojo.data.api.Read API and reads
11         //              data from files in CSV (Comma Separated Values) format.
12         //              All values are simple string values. References to other items
13         //              are not supported as attribute values in this datastore.
14         //
15         //              Example data file:
16         //              name, color, age, tagline
17         //              Kermit, green, 12, "Hi, I'm Kermit the Frog."
18         //              Fozzie Bear, orange, 10, "Wakka Wakka Wakka!"
19         //              Miss Piggy, pink, 11, "Kermie!"
20         //
21         //              Note that values containing a comma must be enclosed with quotes ("")
22         //              Also note that values containing quotes must be escaped with two consecutive quotes (""quoted"")
23         
24         /* examples:
25          *   var csvStore = new dojox.data.CsvStore({url:"movies.csv");
26          *   var csvStore = new dojox.data.CsvStore({url:"http://example.com/movies.csv");
27          */
28
29         constructor: function(/* Object */ keywordParameters){
30                 // summary: initializer
31                 // keywordParameters: {url: String}
32                 // keywordParameters: {data: String}
33                 // keywordParameters: {label: String} The column label for the column to use for the label returned by getLabel.
34                 
35                 this._attributes = [];                  // e.g. ["Title", "Year", "Producer"]
36                 this._attributeIndexes = {};    // e.g. {Title: 0, Year: 1, Producer: 2}
37                 this._dataArray = [];                   // e.g. [[<Item0>],[<Item1>],[<Item2>]]
38                 this._arrayOfAllItems = [];             // e.g. [{_csvId:0,_csvStore:store},...]
39                 this._loadFinished = false;
40                 if(keywordParameters.url){
41                         this.url = keywordParameters.url;
42                 }
43                 this._csvData = keywordParameters.data;
44                 if(keywordParameters.label){
45                         this.label = keywordParameters.label;
46                 }else if(this.label === ""){
47                         this.label = undefined;
48                 }
49                 this._storeProp = "_csvStore";  // Property name for the store reference on every item.
50                 this._idProp = "_csvId";                // Property name for the Item Id on every item.
51                 this._features = {      
52                         'dojo.data.api.Read': true,
53                         'dojo.data.api.Identity': true 
54                 };
55                 this._loadInProgress = false;   //Got to track the initial load to prevent duelling loads of the dataset.
56                 this._queuedFetches = [];
57         },
58
59         url: "", //Declarative hook for setting Csv source url.
60
61         label: "", //Declarative hook for setting the label attribute. 
62         
63         _assertIsItem: function(/* item */ item){
64                 //      summary:
65                 //      This function tests whether the item passed in is indeed an item in the store.
66                 //      item: 
67                 //              The item to test for being contained by the store.
68                 if(!this.isItem(item)){ 
69                         throw new Error("dojox.data.CsvStore: a function was passed an item argument that was not an item");
70                 }
71         },
72         
73         _assertIsAttribute: function(/* item || String */ attribute){
74                 //      summary:
75                 //      This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
76                 //      attribute: 
77                 //              The attribute to test for being contained by the store.
78                 if(!dojo.isString(attribute)){ 
79                         throw new Error("dojox.data.CsvStore: a function was passed an attribute argument that was not an attribute object nor an attribute name string");
80                 }
81         },
82
83 /***************************************
84      dojo.data.api.Read API
85 ***************************************/
86         getValue: function(     /* item */ item, 
87                                                 /* attribute || attribute-name-string */ attribute, 
88                                                 /* value? */ defaultValue){
89                 //      summary: 
90                 //      See dojo.data.api.Read.getValue()
91                 //              Note that for the CsvStore, an empty string value is the same as no value, 
92                 //              so the defaultValue would be returned instead of an empty string.
93                 this._assertIsItem(item);
94                 this._assertIsAttribute(attribute);
95                 var itemValue = defaultValue;
96                 if(this.hasAttribute(item, attribute)){
97                         var itemData = this._dataArray[this.getIdentity(item)];
98                         itemValue = itemData[this._attributeIndexes[attribute]];
99                 }
100                 return itemValue; //String
101         },
102
103         getValues: function(/* item */ item, 
104                                                 /* attribute || attribute-name-string */ attribute){
105                 //      summary: 
106                 //              See dojo.data.api.Read.getValues()
107                 //              CSV syntax does not support multi-valued attributes, so this is just a
108                 //              wrapper function for getValue().
109                 var value = this.getValue(item, attribute);
110                 return (value ? [value] : []); //Array
111         },
112
113         getAttributes: function(/* item */ item){
114                 //      summary: 
115                 //              See dojo.data.api.Read.getAttributes()
116                 this._assertIsItem(item);
117                 var attributes = [];
118                 var itemData = this._dataArray[this.getIdentity(item)];
119                 for(var i=0; i<itemData.length; i++){
120                         // Check for empty string values. CsvStore treats empty strings as no value.
121                         if(itemData[i] != ""){
122                                 attributes.push(this._attributes[i]);
123                         }
124                 }
125                 return attributes; //Array
126         },
127
128         hasAttribute: function( /* item */ item,
129                                                         /* attribute || attribute-name-string */ attribute){
130                 //      summary: 
131                 //              See dojo.data.api.Read.hasAttribute()
132                 //              The hasAttribute test is true if attribute has an index number within the item's array length
133                 //              AND if the item has a value for that attribute. Note that for the CsvStore, an
134                 //              empty string value is the same as no value.
135                 this._assertIsItem(item);
136                 this._assertIsAttribute(attribute);
137                 var attributeIndex = this._attributeIndexes[attribute];
138                 var itemData = this._dataArray[this.getIdentity(item)];
139                 return (typeof attributeIndex != "undefined" && attributeIndex < itemData.length && itemData[attributeIndex] != ""); //Boolean
140         },
141
142         containsValue: function(/* item */ item, 
143                                                         /* attribute || attribute-name-string */ attribute, 
144                                                         /* anything */ value){
145                 //      summary: 
146                 //              See dojo.data.api.Read.containsValue()
147                 var regexp = undefined;
148                 if(typeof value === "string"){
149                    regexp = dojo.data.util.filter.patternToRegExp(value, false);
150                 }
151                 return this._containsValue(item, attribute, value, regexp); //boolean.
152         },
153
154         _containsValue: function(       /* item */ item, 
155                                                                 /* attribute || attribute-name-string */ attribute, 
156                                                                 /* anything */ value,
157                                                                 /* RegExp?*/ regexp){
158                 //      summary: 
159                 //              Internal function for looking at the values contained by the item.
160                 //      description: 
161                 //              Internal function for looking at the values contained by the item.  This 
162                 //              function allows for denoting if the comparison should be case sensitive for
163                 //              strings or not (for handling filtering cases where string case should not matter)
164                 //      
165                 //      item:
166                 //              The data item to examine for attribute values.
167                 //      attribute:
168                 //              The attribute to inspect.
169                 //      value:  
170                 //              The value to match.
171                 //      regexp:
172                 //              Optional regular expression generated off value if value was of string type to handle wildcarding.
173                 //              If present and attribute values are string, then it can be used for comparison instead of 'value'
174                 var values = this.getValues(item, attribute);
175                 for(var i = 0; i < values.length; ++i){
176                         var possibleValue = values[i];
177                         if(typeof possibleValue === "string" && regexp){
178                                 return (possibleValue.match(regexp) !== null);
179                         }else{
180                                 //Non-string matching.
181                                 if(value === possibleValue){
182                                         return true; // Boolean
183                                 }
184                         }
185                 }
186                 return false; // Boolean
187         },
188
189         isItem: function(/* anything */ something){
190                 //      summary: 
191                 //              See dojo.data.api.Read.isItem()
192                 if(something && something[this._storeProp] === this){
193                         var identity = something[this._idProp];
194                         if(identity >= 0 && identity < this._dataArray.length){
195                                 return true; //Boolean
196                         }
197                 }
198                 return false; //Boolean
199         },
200
201         isItemLoaded: function(/* anything */ something){
202                 //      summary: 
203                 //              See dojo.data.api.Read.isItemLoaded()
204                 //              The CsvStore always loads all items, so if it's an item, then it's loaded.
205                 return this.isItem(something); //Boolean
206         },
207
208         loadItem: function(/* item */ item){
209                 //      summary: 
210                 //              See dojo.data.api.Read.loadItem()
211                 //      description:
212                 //              The CsvStore always loads all items, so if it's an item, then it's loaded.
213                 //              From the dojo.data.api.Read.loadItem docs:
214                 //                      If a call to isItemLoaded() returns true before loadItem() is even called,
215                 //                      then loadItem() need not do any work at all and will not even invoke
216                 //                      the callback handlers.
217         },
218
219         getFeatures: function(){
220                 //      summary: 
221                 //              See dojo.data.api.Read.getFeatures()
222                 return this._features; //Object
223         },
224
225         getLabel: function(/* item */ item){
226                 //      summary: 
227                 //              See dojo.data.api.Read.getLabel()
228                 if(this.label && this.isItem(item)){
229                         return this.getValue(item,this.label); //String
230                 }
231                 return undefined; //undefined
232         },
233
234         getLabelAttributes: function(/* item */ item){
235                 //      summary: 
236                 //              See dojo.data.api.Read.getLabelAttributes()
237                 if(this.label){
238                         return [this.label]; //array
239                 }
240                 return null; //null
241         },
242
243
244         // The dojo.data.api.Read.fetch() function is implemented as
245         // a mixin from dojo.data.util.simpleFetch.
246         // That mixin requires us to define _fetchItems().
247         _fetchItems: function(  /* Object */ keywordArgs, 
248                                                         /* Function */ findCallback, 
249                                                         /* Function */ errorCallback){
250                 //      summary: 
251                 //              See dojo.data.util.simpleFetch.fetch()
252                 
253                 var self = this;
254
255                 var filter = function(requestArgs, arrayOfAllItems){
256                         var items = null;
257                         if(requestArgs.query){
258                                 items = [];
259                                 var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false; 
260
261                                 //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the
262                                 //same value for each item examined.  Much more efficient.
263                                 var regexpList = {};
264                                 for(var key in requestArgs.query){
265                                         var value = requestArgs.query[key];
266                                         if(typeof value === "string"){
267                                                 regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
268                                         }
269                                 }
270
271                                 for(var i = 0; i < arrayOfAllItems.length; ++i){
272                                         var match = true;
273                                         var candidateItem = arrayOfAllItems[i];
274                                         for(var key in requestArgs.query){
275                                                 var value = requestArgs.query[key];
276                                                 if(!self._containsValue(candidateItem, key, value, regexpList[key])){
277                                                         match = false;
278                                                 }
279                                         }
280                                         if(match){
281                                                 items.push(candidateItem);
282                                         }
283                                 }
284                         }else{
285                                 // We want a copy to pass back in case the parent wishes to sort the array.  We shouldn't allow resort 
286                                 // of the internal list so that multiple callers can get lists and sort without affecting each other.
287                                 if(arrayOfAllItems.length> 0){
288                                         items = arrayOfAllItems.slice(0,arrayOfAllItems.length); 
289                                 }
290                         }
291                         findCallback(items, requestArgs);
292                 };
293
294                 if(this._loadFinished){
295                         filter(keywordArgs, this._arrayOfAllItems);
296                 }else{
297                         if(this.url !== ""){
298                                 //If fetches come in before the loading has finished, but while
299                                 //a load is in progress, we have to defer the fetching to be 
300                                 //invoked in the callback.
301                                 if(this._loadInProgress){
302                                         this._queuedFetches.push({args: keywordArgs, filter: filter});
303                                 }else{
304                                         this._loadInProgress = true;
305                                         var getArgs = {
306                                                         url: self.url, 
307                                                         handleAs: "text"
308                                                 };
309                                         var getHandler = dojo.xhrGet(getArgs);
310                                         getHandler.addCallback(function(data){
311                                                 self._processData(data);
312                                                 filter(keywordArgs, self._arrayOfAllItems);
313                                                 self._handleQueuedFetches();
314                                         });
315                                         getHandler.addErrback(function(error){
316                                                 self._loadInProgress = false;
317                                                 if(errorCallback){
318                                                         errorCallback(error, keywordArgs);
319                                                 }else{
320                                                         throw error;
321                                                 }
322                                         });
323                                 }
324                         }else if(this._csvData){
325                                 this._processData(this._csvData);
326                                 this._csvData = null;
327                                 filter(keywordArgs, this._arrayOfAllItems);
328                         }else{
329                                 var error = new Error("dojox.data.CsvStore: No CSV source data was provided as either URL or String data input.");
330                                 if(errorCallback){
331                                         errorCallback(error, keywordArgs);
332                                 }else{
333                                         throw error;
334                                 }
335                         }
336                 }
337         },
338         
339         close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
340                  //     summary: 
341                  //             See dojo.data.api.Read.close()
342         },
343         
344         
345         // -------------------------------------------------------------------
346         // Private methods
347         _getArrayOfArraysFromCsvFileContents: function(/* string */ csvFileContents){
348                 /* summary:
349                  *   Parses a string of CSV records into a nested array structure.
350                  * description:
351                  *   Given a string containing CSV records, this method parses
352                  *   the string and returns a data structure containing the parsed
353                  *   content.  The data structure we return is an array of length
354                  *   R, where R is the number of rows (lines) in the CSV data.  The 
355                  *   return array contains one sub-array for each CSV line, and each 
356                  *   sub-array contains C string values, where C is the number of 
357                  *   columns in the CSV data.
358                  */
359                  
360                 /* example:
361                  *   For example, given this CSV string as input:
362                  *     "Title, Year, Producer \n Alien, 1979, Ridley Scott \n Blade Runner, 1982, Ridley Scott"
363                  *   this._dataArray will be set to:
364                  *     [["Alien", "1979", "Ridley Scott"],
365                  *      ["Blade Runner", "1982", "Ridley Scott"]]
366                  *   And this._attributes will be set to:
367                  *     ["Title", "Year", "Producer"]
368                  *   And this._attributeIndexes will be set to:
369                  *     { "Title":0, "Year":1, "Producer":2 }
370                  */
371                 if(dojo.isString(csvFileContents)){
372                         var lineEndingCharacters = new RegExp("\r\n|\n|\r");
373                         var leadingWhiteSpaceCharacters = new RegExp("^\\s+",'g');
374                         var trailingWhiteSpaceCharacters = new RegExp("\\s+$",'g');
375                         var doubleQuotes = new RegExp('""','g');
376                         var arrayOfOutputRecords = [];
377                         
378                         var arrayOfInputLines = csvFileContents.split(lineEndingCharacters);
379                         for(var i = 0; i < arrayOfInputLines.length; ++i){
380                                 var singleLine = arrayOfInputLines[i];
381                                 if(singleLine.length > 0){
382                                         var listOfFields = singleLine.split(',');
383                                         var j = 0;
384                                         while(j < listOfFields.length){
385                                                 var space_field_space = listOfFields[j];
386                                                 var field_space = space_field_space.replace(leadingWhiteSpaceCharacters, ''); // trim leading whitespace
387                                                 var field = field_space.replace(trailingWhiteSpaceCharacters, ''); // trim trailing whitespace
388                                                 var firstChar = field.charAt(0);
389                                                 var lastChar = field.charAt(field.length - 1);
390                                                 var secondToLastChar = field.charAt(field.length - 2);
391                                                 var thirdToLastChar = field.charAt(field.length - 3);
392                                                 if(field.length === 2 && field == "\"\""){
393                                                         listOfFields[j] = "";  //Special case empty string field.
394                                                 }else if((firstChar == '"') && 
395                                                                 ((lastChar != '"') || 
396                                                                  ((lastChar == '"') && (secondToLastChar == '"') && (thirdToLastChar != '"')))){
397                                                         if(j+1 === listOfFields.length){
398                                                                 // alert("The last field in record " + i + " is corrupted:\n" + field);
399                                                                 return null; //null
400                                                         }
401                                                         var nextField = listOfFields[j+1];
402                                                         listOfFields[j] = field_space + ',' + nextField;
403                                                         listOfFields.splice(j+1, 1); // delete element [j+1] from the list
404                                                 }else{
405                                                         if((firstChar == '"') && (lastChar == '"')){
406                                                                 field = field.slice(1, (field.length - 1)); // trim the " characters off the ends
407                                                                 field = field.replace(doubleQuotes, '"');   // replace "" with "
408                                                         }
409                                                         listOfFields[j] = field;
410                                                         j += 1;
411                                                 }
412                                         }
413                                         arrayOfOutputRecords.push(listOfFields);
414                                 }
415                         }
416                         
417                         // The first item of the array must be the header row with attribute names.
418                         this._attributes = arrayOfOutputRecords.shift();
419                         for(var i=0; i<this._attributes.length; i++){
420                                 // Store the index of each attribute 
421                                 this._attributeIndexes[this._attributes[i]] = i;
422                         }
423                         this._dataArray = arrayOfOutputRecords; //Array
424                 }
425         },
426         
427         _processData: function(/* String */ data){
428                 this._getArrayOfArraysFromCsvFileContents(data);
429                 this._arrayOfAllItems = [];
430                 for(var i=0; i<this._dataArray.length; i++){
431                         this._arrayOfAllItems.push(this._createItemFromIdentity(i));
432                 }
433                 this._loadFinished = true;
434                 this._loadInProgress = false;
435         },
436         
437         _createItemFromIdentity: function(/* String */ identity){
438                 var item = {};
439                 item[this._storeProp] = this;
440                 item[this._idProp] = identity;
441                 return item; //Object
442         },
443         
444         
445 /***************************************
446      dojo.data.api.Identity API
447 ***************************************/
448         getIdentity: function(/* item */ item){
449                 //      summary: 
450                 //              See dojo.data.api.Identity.getIdentity()
451                 if(this.isItem(item)){
452                         return item[this._idProp]; //String
453                 }
454                 return null; //null
455         },
456
457         fetchItemByIdentity: function(/* Object */ keywordArgs){
458                 //      summary: 
459                 //              See dojo.data.api.Identity.fetchItemByIdentity()
460
461                 //Hasn't loaded yet, we have to trigger the load.
462                 
463
464                 if(!this._loadFinished){
465                         var self = this;
466                         if(this.url !== ""){
467                                 //If fetches come in before the loading has finished, but while
468                                 //a load is in progress, we have to defer the fetching to be 
469                                 //invoked in the callback.
470                                 if(this._loadInProgress){
471                                         this._queuedFetches.push({args: keywordArgs});
472                                 }else{
473                                         this._loadInProgress = true;
474                                         var getArgs = {
475                                                         url: self.url, 
476                                                         handleAs: "text"
477                                                 };
478                                         var getHandler = dojo.xhrGet(getArgs);
479                                         getHandler.addCallback(function(data){
480                                                 var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
481                                                 try{
482                                                         self._processData(data);
483                                                         var item = self._createItemFromIdentity(keywordArgs.identity);
484                                                         if(!self.isItem(item)){
485                                                                 item = null;
486                                                         }
487                                                         if(keywordArgs.onItem){
488                                                                 keywordArgs.onItem.call(scope, item);
489                                                         }
490                                                         self._handleQueuedFetches();
491                                                 }catch(error){
492                                                         if(keywordArgs.onError){
493                                                                 keywordArgs.onError.call(scope, error);
494                                                         }
495                                                 }
496                                         });
497                                         getHandler.addErrback(function(error){
498                                                 this._loadInProgress = false;
499                                                 if(keywordArgs.onError){
500                                                         var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
501                                                         keywordArgs.onError.call(scope, error);
502                                                 }
503                                         });
504                                 }
505                         }else if(this._csvData){
506                                 self._processData(self._csvData);
507                                 self._csvData = null;
508                                 var item = self._createItemFromIdentity(keywordArgs.identity);
509                                 if(!self.isItem(item)){
510                                         item = null;
511                                 }
512                                 if(keywordArgs.onItem){
513                                         var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
514                                         keywordArgs.onItem.call(scope, item);
515                                 }
516                         }
517                 }else{
518                         //Already loaded.  We can just look it up and call back.
519                         var item = this._createItemFromIdentity(keywordArgs.identity);
520                         if(!this.isItem(item)){
521                                 item = null;
522                         }
523                         if(keywordArgs.onItem){
524                                 var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
525                                 keywordArgs.onItem.call(scope, item);
526                         }
527                 }
528         },
529
530         getIdentityAttributes: function(/* item */ item){
531                  //     summary: 
532                  //             See dojo.data.api.Identity.getIdentifierAttributes()
533                  
534                  //Identity isn't a public attribute in the item, it's the row position index.
535                  //So, return null.
536                  return null;
537         },
538
539         _handleQueuedFetches: function(){
540                 //      summary: 
541                 //              Internal function to execute delayed request in the store.
542                 //Execute any deferred fetches now.
543                 if (this._queuedFetches.length > 0) {
544                         for(var i = 0; i < this._queuedFetches.length; i++){
545                                 var fData = this._queuedFetches[i];
546                                 var delayedFilter = fData.filter;
547                                 var delayedQuery = fData.args;
548                                 if(delayedFilter){
549                                         delayedFilter(delayedQuery, this._arrayOfAllItems); 
550                                 }else{
551                                         this.fetchItemByIdentity(fData.args);
552                                 }
553                         }
554                         this._queuedFetches = [];
555                 }
556         }
557 });
558 //Mix in the simple fetch implementation to this class.
559 dojo.extend(dojox.data.CsvStore,dojo.data.util.simpleFetch);
560
561 }