1 if(!dojo._hasResource["dojo.data.ItemFileReadStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2 dojo._hasResource["dojo.data.ItemFileReadStore"] = true;
3 dojo.provide("dojo.data.ItemFileReadStore");
5 dojo.require("dojo.data.util.filter");
6 dojo.require("dojo.data.util.simpleFetch");
7 dojo.require("dojo.date.stamp");
9 dojo.declare("dojo.data.ItemFileReadStore", null,{
11 // The ItemFileReadStore implements the dojo.data.api.Read API and reads
12 // data from JSON files that have contents in this format --
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'}
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.
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:
28 // type0: function || object,
29 // type1: function || object,
31 // typeN: function || object
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:
37 // type: function, //constructor.
38 // deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately.
41 this._arrayOfAllItems = [];
42 this._arrayOfTopLevelItems = [];
43 this._loadFinished = false;
44 this._jsonFileUrl = keywordParameters.url;
45 this._jsonData = keywordParameters.data;
46 this._datatypeMap = keywordParameters.typeMap || {};
47 if(!this._datatypeMap['Date']){
48 //If no default mapping for dates, then set this as default.
49 //We use the dojo.date.stamp here because the ISO format is the 'dojo way'
50 //of generically representing dates.
51 this._datatypeMap['Date'] = {
53 deserialize: function(value){
54 return dojo.date.stamp.fromISOString(value);
58 this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true};
59 this._itemsByIdentity = null;
60 this._storeRefPropName = "_S"; // Default name for the store reference to attach to every item.
61 this._itemNumPropName = "_0"; // Default Item Id for isItem to attach to every item.
62 this._rootItemPropName = "_RI"; // Default Item Id for isItem to attach to every item.
63 this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity
64 this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset.
65 this._queuedFetches = [];
68 url: "", // use "" rather than undefined for the benefit of the parser (#3539)
70 _assertIsItem: function(/* item */ item){
72 // This function tests whether the item passed in is indeed an item in the store.
74 // The item to test for being contained by the store.
75 if(!this.isItem(item)){
76 throw new Error("dojo.data.ItemFileReadStore: Invalid item argument.");
80 _assertIsAttribute: function(/* attribute-name-string */ attribute){
82 // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
84 // The attribute to test for being contained by the store.
85 if(typeof attribute !== "string"){
86 throw new Error("dojo.data.ItemFileReadStore: Invalid attribute argument.");
90 getValue: function( /* item */ item,
91 /* attribute-name-string */ attribute,
92 /* value? */ defaultValue){
94 // See dojo.data.api.Read.getValue()
95 var values = this.getValues(item, attribute);
96 return (values.length > 0)?values[0]:defaultValue; // mixed
99 getValues: function(/* item */ item,
100 /* attribute-name-string */ attribute){
102 // See dojo.data.api.Read.getValues()
104 this._assertIsItem(item);
105 this._assertIsAttribute(attribute);
106 return item[attribute] || []; // Array
109 getAttributes: function(/* item */ item){
111 // See dojo.data.api.Read.getAttributes()
112 this._assertIsItem(item);
114 for(var key in item){
115 // Save off only the real item attributes, not the special id marks for O(1) isItem.
116 if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){
117 attributes.push(key);
120 return attributes; // Array
123 hasAttribute: function( /* item */ item,
124 /* attribute-name-string */ attribute) {
126 // See dojo.data.api.Read.hasAttribute()
127 return this.getValues(item, attribute).length > 0;
130 containsValue: function(/* item */ item,
131 /* attribute-name-string */ attribute,
132 /* anything */ value){
134 // See dojo.data.api.Read.containsValue()
135 var regexp = undefined;
136 if(typeof value === "string"){
137 regexp = dojo.data.util.filter.patternToRegExp(value, false);
139 return this._containsValue(item, attribute, value, regexp); //boolean.
142 _containsValue: function( /* item */ item,
143 /* attribute-name-string */ attribute,
144 /* anything */ value,
145 /* RegExp?*/ regexp){
147 // Internal function for looking at the values contained by the item.
149 // Internal function for looking at the values contained by the item. This
150 // function allows for denoting if the comparison should be case sensitive for
151 // strings or not (for handling filtering cases where string case should not matter)
154 // The data item to examine for attribute values.
156 // The attribute to inspect.
158 // The value to match.
160 // Optional regular expression generated off value if value was of string type to handle wildcarding.
161 // If present and attribute values are string, then it can be used for comparison instead of 'value'
162 return dojo.some(this.getValues(item, attribute), function(possibleValue){
163 if(possibleValue !== null && !dojo.isObject(possibleValue) && regexp){
164 if(possibleValue.toString().match(regexp)){
165 return true; // Boolean
167 }else if(value === possibleValue){
168 return true; // Boolean
173 isItem: function(/* anything */ something){
175 // See dojo.data.api.Read.isItem()
176 if(something && something[this._storeRefPropName] === this){
177 if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){
181 return false; // Boolean
184 isItemLoaded: function(/* anything */ something){
186 // See dojo.data.api.Read.isItemLoaded()
187 return this.isItem(something); //boolean
190 loadItem: function(/* object */ keywordArgs){
192 // See dojo.data.api.Read.loadItem()
193 this._assertIsItem(keywordArgs.item);
196 getFeatures: function(){
198 // See dojo.data.api.Read.getFeatures()
199 return this._features; //Object
202 getLabel: function(/* item */ item){
204 // See dojo.data.api.Read.getLabel()
205 if(this._labelAttr && this.isItem(item)){
206 return this.getValue(item,this._labelAttr); //String
208 return undefined; //undefined
211 getLabelAttributes: function(/* item */ item){
213 // See dojo.data.api.Read.getLabelAttributes()
215 return [this._labelAttr]; //array
220 _fetchItems: function( /* Object */ keywordArgs,
221 /* Function */ findCallback,
222 /* Function */ errorCallback){
224 // See dojo.data.util.simpleFetch.fetch()
226 var filter = function(requestArgs, arrayOfItems){
228 if(requestArgs.query){
229 var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false;
231 //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the
232 //same value for each item examined. Much more efficient.
234 for(var key in requestArgs.query){
235 var value = requestArgs.query[key];
236 if(typeof value === "string"){
237 regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
241 for(var i = 0; i < arrayOfItems.length; ++i){
243 var candidateItem = arrayOfItems[i];
244 if(candidateItem === null){
247 for(var key in requestArgs.query) {
248 var value = requestArgs.query[key];
249 if (!self._containsValue(candidateItem, key, value, regexpList[key])){
255 items.push(candidateItem);
258 findCallback(items, requestArgs);
260 // We want a copy to pass back in case the parent wishes to sort the array.
261 // We shouldn't allow resort of the internal list, so that multiple callers
262 // can get lists and sort without affecting each other. We also need to
263 // filter out any null values that have been left as a result of deleteItem()
264 // calls in ItemFileWriteStore.
265 for(var i = 0; i < arrayOfItems.length; ++i){
266 var item = arrayOfItems[i];
271 findCallback(items, requestArgs);
275 if(this._loadFinished){
276 filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
279 if(this._jsonFileUrl){
280 //If fetches come in before the loading has finished, but while
281 //a load is in progress, we have to defer the fetching to be
282 //invoked in the callback.
283 if(this._loadInProgress){
284 this._queuedFetches.push({args: keywordArgs, filter: filter});
286 this._loadInProgress = true;
288 url: self._jsonFileUrl,
289 handleAs: "json-comment-optional"
291 var getHandler = dojo.xhrGet(getArgs);
292 getHandler.addCallback(function(data){
294 self._getItemsFromLoadedData(data);
295 self._loadFinished = true;
296 self._loadInProgress = false;
298 filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions));
299 self._handleQueuedFetches();
301 self._loadFinished = true;
302 self._loadInProgress = false;
303 errorCallback(e, keywordArgs);
306 getHandler.addErrback(function(error){
307 self._loadInProgress = false;
308 errorCallback(error, keywordArgs);
311 }else if(this._jsonData){
313 this._loadFinished = true;
314 this._getItemsFromLoadedData(this._jsonData);
315 this._jsonData = null;
316 filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
318 errorCallback(e, keywordArgs);
321 errorCallback(new Error("dojo.data.ItemFileReadStore: No JSON source data was provided as either URL or a nested Javascript object."), keywordArgs);
326 _handleQueuedFetches: function(){
328 // Internal function to execute delayed request in the store.
329 //Execute any deferred fetches now.
330 if (this._queuedFetches.length > 0) {
331 for(var i = 0; i < this._queuedFetches.length; i++){
332 var fData = this._queuedFetches[i];
333 var delayedQuery = fData.args;
334 var delayedFilter = fData.filter;
336 delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions));
338 this.fetchItemByIdentity(delayedQuery);
341 this._queuedFetches = [];
345 _getItemsArray: function(/*object?*/queryOptions){
347 // Internal function to determine which list of items to search over.
348 // queryOptions: The query options parameter, if any.
349 if(queryOptions && queryOptions.deep) {
350 return this._arrayOfAllItems;
352 return this._arrayOfTopLevelItems;
355 close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
357 // See dojo.data.api.Read.close()
360 _getItemsFromLoadedData: function(/* Object */ dataObject){
362 // Function to parse the loaded data into item format and build the internal items array.
364 // Function to parse the loaded data into item format and build the internal items array.
367 // The JS data object containing the raw data to convery into item format.
370 // Array of items in store item format.
372 // First, we define a couple little utility functions...
374 function valueIsAnItem(/* anything */ aValue){
376 // Given any sort of value that could be in the raw json data,
377 // return true if we should interpret the value as being an
378 // item itself, rather than a literal value or a reference.
380 // | false == valueIsAnItem("Kermit");
381 // | false == valueIsAnItem(42);
382 // | false == valueIsAnItem(new Date());
383 // | false == valueIsAnItem({_type:'Date', _value:'May 14, 1802'});
384 // | false == valueIsAnItem({_reference:'Kermit'});
385 // | true == valueIsAnItem({name:'Kermit', color:'green'});
386 // | true == valueIsAnItem({iggy:'pop'});
387 // | true == valueIsAnItem({foo:42});
390 (typeof aValue == "object") &&
391 (!dojo.isArray(aValue)) &&
392 (!dojo.isFunction(aValue)) &&
393 (aValue.constructor == Object) &&
394 (typeof aValue._reference == "undefined") &&
395 (typeof aValue._type == "undefined") &&
396 (typeof aValue._value == "undefined")
402 function addItemAndSubItemsToArrayOfAllItems(/* Item */ anItem){
403 self._arrayOfAllItems.push(anItem);
404 for(var attribute in anItem){
405 var valueForAttribute = anItem[attribute];
406 if(valueForAttribute){
407 if(dojo.isArray(valueForAttribute)){
408 var valueArray = valueForAttribute;
409 for(var k = 0; k < valueArray.length; ++k){
410 var singleValue = valueArray[k];
411 if(valueIsAnItem(singleValue)){
412 addItemAndSubItemsToArrayOfAllItems(singleValue);
416 if(valueIsAnItem(valueForAttribute)){
417 addItemAndSubItemsToArrayOfAllItems(valueForAttribute);
424 this._labelAttr = dataObject.label;
426 // We need to do some transformations to convert the data structure
427 // that we read from the file into a format that will be convenient
428 // to work with in memory.
430 // Step 1: Walk through the object hierarchy and build a list of all items
433 this._arrayOfAllItems = [];
434 this._arrayOfTopLevelItems = dataObject.items;
436 for(i = 0; i < this._arrayOfTopLevelItems.length; ++i){
437 item = this._arrayOfTopLevelItems[i];
438 addItemAndSubItemsToArrayOfAllItems(item);
439 item[this._rootItemPropName]=true;
442 // Step 2: Walk through all the attribute values of all the items,
443 // and replace single values with arrays. For example, we change this:
444 // { name:'Miss Piggy', pets:'Foo-Foo'}
446 // { name:['Miss Piggy'], pets:['Foo-Foo']}
448 // We also store the attribute names so we can validate our store
449 // reference and item id special properties for the O(1) isItem
450 var allAttributeNames = {};
453 for(i = 0; i < this._arrayOfAllItems.length; ++i){
454 item = this._arrayOfAllItems[i];
456 if (key !== this._rootItemPropName)
458 var value = item[key];
460 if(!dojo.isArray(value)){
467 allAttributeNames[key]=key;
471 // Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName
472 // This should go really fast, it will generally never even run the loop.
473 while(allAttributeNames[this._storeRefPropName]){
474 this._storeRefPropName += "_";
476 while(allAttributeNames[this._itemNumPropName]){
477 this._itemNumPropName += "_";
479 while(allAttributeNames[this._reverseRefMap]){
480 this._reverseRefMap += "_";
483 // Step 4: Some data files specify an optional 'identifier', which is
484 // the name of an attribute that holds the identity of each item.
485 // If this data file specified an identifier attribute, then build a
486 // hash table of items keyed by the identity of the items.
489 var identifier = dataObject.identifier;
491 this._itemsByIdentity = {};
492 this._features['dojo.data.api.Identity'] = identifier;
493 for(i = 0; i < this._arrayOfAllItems.length; ++i){
494 item = this._arrayOfAllItems[i];
495 arrayOfValues = item[identifier];
496 var identity = arrayOfValues[0];
497 if(!this._itemsByIdentity[identity]){
498 this._itemsByIdentity[identity] = item;
500 if(this._jsonFileUrl){
501 throw new Error("dojo.data.ItemFileReadStore: The json data as specified by: [" + this._jsonFileUrl + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]");
502 }else if(this._jsonData){
503 throw new Error("dojo.data.ItemFileReadStore: The json data provided by the creation arguments is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]");
508 this._features['dojo.data.api.Identity'] = Number;
511 // Step 5: Walk through all the items, and set each item's properties
512 // for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true.
513 for(i = 0; i < this._arrayOfAllItems.length; ++i){
514 item = this._arrayOfAllItems[i];
515 item[this._storeRefPropName] = this;
516 item[this._itemNumPropName] = i;
519 // Step 6: We walk through all the attribute values of all the items,
520 // looking for type/value literals and item-references.
522 // We replace item-references with pointers to items. For example, we change:
523 // { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
525 // { name:['Kermit'], friends:[miss_piggy] }
526 // (where miss_piggy is the object representing the 'Miss Piggy' item).
528 // We replace type/value pairs with typed-literals. For example, we change:
529 // { name:['Nelson Mandela'], born:[{_type:'Date', _value:'July 18, 1918'}] }
531 // { name:['Kermit'], born:(new Date('July 18, 1918')) }
533 // We also generate the associate map for all items for the O(1) isItem function.
534 for(i = 0; i < this._arrayOfAllItems.length; ++i){
535 item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
537 arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}]
538 for(var j = 0; j < arrayOfValues.length; ++j) {
539 value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}}
540 if(value !== null && typeof value == "object"){
541 if(value._type && value._value){
542 var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber'
543 var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}}
545 throw new Error("dojo.data.ItemFileReadStore: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'");
546 }else if(dojo.isFunction(mappingObj)){
547 arrayOfValues[j] = new mappingObj(value._value);
548 }else if(dojo.isFunction(mappingObj.deserialize)){
549 arrayOfValues[j] = mappingObj.deserialize(value._value);
551 throw new Error("dojo.data.ItemFileReadStore: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function");
554 if(value._reference){
555 var referenceDescription = value._reference; // example: {name:'Miss Piggy'}
556 if(!dojo.isObject(referenceDescription)){
557 // example: 'Miss Piggy'
558 // from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]}
559 arrayOfValues[j] = this._itemsByIdentity[referenceDescription];
561 // example: {name:'Miss Piggy'}
562 // from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
563 for(var k = 0; k < this._arrayOfAllItems.length; ++k){
564 var candidateItem = this._arrayOfAllItems[k];
566 for(var refKey in referenceDescription){
567 if(candidateItem[refKey] != referenceDescription[refKey]){
572 arrayOfValues[j] = candidateItem;
576 if(this.referenceIntegrity){
577 var refItem = arrayOfValues[j];
578 if(this.isItem(refItem)){
579 this._addReferenceToMap(refItem, item, key);
582 }else if(this.isItem(value)){
583 //It's a child item (not one referenced through _reference).
584 //We need to treat this as a referenced item, so it can be cleaned up
585 //in a write store easily.
586 if(this.referenceIntegrity){
587 this._addReferenceToMap(value, item, key);
596 _addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){
598 // Method to add an reference map entry for an item and attribute.
600 // Method to add an reference map entry for an item and attribute. //
602 // The item that is referenced.
604 // The item that holds the new reference to refItem.
606 // The attribute on parentItem that contains the new reference.
608 //Stub function, does nothing. Real processing is in ItemFileWriteStore.
611 getIdentity: function(/* item */ item){
613 // See dojo.data.api.Identity.getIdentity()
614 var identifier = this._features['dojo.data.api.Identity'];
615 if(identifier === Number){
616 return item[this._itemNumPropName]; // Number
618 var arrayOfValues = item[identifier];
620 return arrayOfValues[0]; // Object || String
626 fetchItemByIdentity: function(/* Object */ keywordArgs){
628 // See dojo.data.api.Identity.fetchItemByIdentity()
630 // Hasn't loaded yet, we have to trigger the load.
631 if(!this._loadFinished){
633 if(this._jsonFileUrl){
635 if(this._loadInProgress){
636 this._queuedFetches.push({args: keywordArgs});
638 this._loadInProgress = true;
640 url: self._jsonFileUrl,
641 handleAs: "json-comment-optional"
643 var getHandler = dojo.xhrGet(getArgs);
644 getHandler.addCallback(function(data){
645 var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
647 self._getItemsFromLoadedData(data);
648 self._loadFinished = true;
649 self._loadInProgress = false;
650 var item = self._getItemByIdentity(keywordArgs.identity);
651 if(keywordArgs.onItem){
652 keywordArgs.onItem.call(scope, item);
654 self._handleQueuedFetches();
656 self._loadInProgress = false;
657 if(keywordArgs.onError){
658 keywordArgs.onError.call(scope, error);
662 getHandler.addErrback(function(error){
663 self._loadInProgress = false;
664 if(keywordArgs.onError){
665 var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
666 keywordArgs.onError.call(scope, error);
671 }else if(this._jsonData){
672 // Passed in data, no need to xhr.
673 self._getItemsFromLoadedData(self._jsonData);
674 self._jsonData = null;
675 self._loadFinished = true;
676 var item = self._getItemByIdentity(keywordArgs.identity);
677 if(keywordArgs.onItem){
678 var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
679 keywordArgs.onItem.call(scope, item);
683 // Already loaded. We can just look it up and call back.
684 var item = this._getItemByIdentity(keywordArgs.identity);
685 if(keywordArgs.onItem){
686 var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
687 keywordArgs.onItem.call(scope, item);
692 _getItemByIdentity: function(/* Object */ identity){
694 // Internal function to look an item up by its identity map.
696 if(this._itemsByIdentity){
697 item = this._itemsByIdentity[identity];
699 item = this._arrayOfAllItems[identity];
701 if(item === undefined){
704 return item; // Object
707 getIdentityAttributes: function(/* item */ item){
709 // See dojo.data.api.Identity.getIdentifierAttributes()
711 var identifier = this._features['dojo.data.api.Identity'];
712 if(identifier === Number){
713 // If (identifier === Number) it means getIdentity() just returns
714 // an integer item-number for each item. The dojo.data.api.Identity
715 // spec says we need to return null if the identity is not composed
719 return [identifier]; // Array
723 _forceLoad: function(){
725 // Internal function to force a load of the store if it hasn't occurred yet. This is required
726 // for specific functions to work properly.
728 if(this._jsonFileUrl){
730 url: self._jsonFileUrl,
731 handleAs: "json-comment-optional",
734 var getHandler = dojo.xhrGet(getArgs);
735 getHandler.addCallback(function(data){
737 //Check to be sure there wasn't another load going on concurrently
738 //So we don't clobber data that comes in on it. If there is a load going on
739 //then do not save this data. It will potentially clobber current data.
740 //We mainly wanted to sync/wait here.
741 //TODO: Revisit the loading scheme of this store to improve multi-initial
743 if (self._loadInProgress !== true && !self._loadFinished) {
744 self._getItemsFromLoadedData(data);
745 self._loadFinished = true;
752 getHandler.addErrback(function(error){
755 }else if(this._jsonData){
756 self._getItemsFromLoadedData(self._jsonData);
757 self._jsonData = null;
758 self._loadFinished = true;
762 //Mix in the simple fetch implementation to this class.
763 dojo.extend(dojo.data.ItemFileReadStore,dojo.data.util.simpleFetch);