]> git.pond.sub.org Git - eow/blob - static/dojo-release-1.1.1/dojo/data/ItemFileWriteStore.js
Comment class stub
[eow] / static / dojo-release-1.1.1 / dojo / data / ItemFileWriteStore.js
1 if(!dojo._hasResource["dojo.data.ItemFileWriteStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2 dojo._hasResource["dojo.data.ItemFileWriteStore"] = true;
3 dojo.provide("dojo.data.ItemFileWriteStore");
4 dojo.require("dojo.data.ItemFileReadStore");
5
6 dojo.declare("dojo.data.ItemFileWriteStore", dojo.data.ItemFileReadStore, {
7         constructor: function(/* object */ keywordParameters){
8                 //      keywordParameters: {typeMap: object)
9                 //              The structure of the typeMap object is as follows:
10                 //              {
11                 //                      type0: function || object,
12                 //                      type1: function || object,
13                 //                      ...
14                 //                      typeN: function || object
15                 //              }
16                 //              Where if it is a function, it is assumed to be an object constructor that takes the 
17                 //              value of _value as the initialization parameters.  It is serialized assuming object.toString()
18                 //              serialization.  If it is an object, then it is assumed
19                 //              to be an object of general form:
20                 //              {
21                 //                      type: function, //constructor.
22                 //                      deserialize:    function(value) //The function that parses the value and constructs the object defined by type appropriately.
23                 //                      serialize:      function(object) //The function that converts the object back into the proper file format form.
24                 //              }
25
26                 // ItemFileWriteStore extends ItemFileReadStore to implement these additional dojo.data APIs
27                 this._features['dojo.data.api.Write'] = true;
28                 this._features['dojo.data.api.Notification'] = true;
29                 
30                 // For keeping track of changes so that we can implement isDirty and revert
31                 this._pending = {
32                         _newItems:{}, 
33                         _modifiedItems:{}, 
34                         _deletedItems:{}
35                 };
36
37                 if(!this._datatypeMap['Date'].serialize){
38                         this._datatypeMap['Date'].serialize = function(obj){
39                                 return dojo.date.stamp.toISOString(obj, {zulu:true});
40                         };
41                 }
42                 //Disable only if explicitly set to false.
43                 if(keywordParameters && (keywordParameters.referenceIntegrity === false)){
44                         this.referenceIntegrity = false;
45                 }
46
47                 // this._saveInProgress is set to true, briefly, from when save() is first called to when it completes
48                 this._saveInProgress = false;
49         },
50
51         referenceIntegrity: true,  //Flag that defaultly enabled reference integrity tracking.  This way it can also be disabled pogrammatially or declaratively.
52
53         _assert: function(/* boolean */ condition){
54                 if(!condition) {
55                         throw new Error("assertion failed in ItemFileWriteStore");
56                 }
57         },
58
59         _getIdentifierAttribute: function(){
60                 var identifierAttribute = this.getFeatures()['dojo.data.api.Identity'];
61                 // this._assert((identifierAttribute === Number) || (dojo.isString(identifierAttribute)));
62                 return identifierAttribute;
63         },
64         
65         
66 /* dojo.data.api.Write */
67
68         newItem: function(/* Object? */ keywordArgs, /* Object? */ parentInfo){
69                 // summary: See dojo.data.api.Write.newItem()
70
71                 this._assert(!this._saveInProgress);
72
73                 if (!this._loadFinished){
74                         // We need to do this here so that we'll be able to find out what
75                         // identifierAttribute was specified in the data file.
76                         this._forceLoad();
77                 }
78
79                 if(typeof keywordArgs != "object" && typeof keywordArgs != "undefined"){
80                         throw new Error("newItem() was passed something other than an object");
81                 }
82                 var newIdentity = null;
83                 var identifierAttribute = this._getIdentifierAttribute();
84                 if(identifierAttribute === Number){
85                         newIdentity = this._arrayOfAllItems.length;
86                 }else{
87                         newIdentity = keywordArgs[identifierAttribute];
88                         if (typeof newIdentity === "undefined"){
89                                 throw new Error("newItem() was not passed an identity for the new item");
90                         }
91                         if (dojo.isArray(newIdentity)){
92                                 throw new Error("newItem() was not passed an single-valued identity");
93                         }
94                 }
95                 
96                 // make sure this identity is not already in use by another item, if identifiers were 
97                 // defined in the file.  Otherwise it would be the item count, 
98                 // which should always be unique in this case.
99                 if(this._itemsByIdentity){
100                         this._assert(typeof this._itemsByIdentity[newIdentity] === "undefined");
101                 }
102                 this._assert(typeof this._pending._newItems[newIdentity] === "undefined");
103                 this._assert(typeof this._pending._deletedItems[newIdentity] === "undefined");
104                 
105                 var newItem = {};
106                 newItem[this._storeRefPropName] = this;         
107                 newItem[this._itemNumPropName] = this._arrayOfAllItems.length;
108                 if(this._itemsByIdentity){
109                         this._itemsByIdentity[newIdentity] = newItem;
110                         //We have to set the identifier now, otherwise we can't look it
111                         //up at calls to setValueorValues in parentInfo handling.
112                         newItem[identifierAttribute] = [newIdentity];
113                 }
114                 this._arrayOfAllItems.push(newItem);
115
116                 //We need to construct some data for the onNew call too...
117                 var pInfo = null;
118                 
119                 // Now we need to check to see where we want to assign this thingm if any.
120                 if(parentInfo && parentInfo.parent && parentInfo.attribute){
121                         pInfo = {
122                                 item: parentInfo.parent,
123                                 attribute: parentInfo.attribute,
124                                 oldValue: undefined
125                         };
126
127                         //See if it is multi-valued or not and handle appropriately
128                         //Generally, all attributes are multi-valued for this store
129                         //So, we only need to append if there are already values present.
130                         var values = this.getValues(parentInfo.parent, parentInfo.attribute);
131                         if(values && values.length > 0){
132                                 var tempValues = values.slice(0, values.length);
133                                 if(values.length === 1){
134                                         pInfo.oldValue = values[0];
135                                 }else{
136                                         pInfo.oldValue = values.slice(0, values.length);
137                                 }
138                                 tempValues.push(newItem);
139                                 this._setValueOrValues(parentInfo.parent, parentInfo.attribute, tempValues, false);
140                                 pInfo.newValue = this.getValues(parentInfo.parent, parentInfo.attribute);
141                         }else{
142                                 this._setValueOrValues(parentInfo.parent, parentInfo.attribute, newItem, false);
143                                 pInfo.newValue = newItem;
144                         }
145                 }else{
146                         //Toplevel item, add to both top list as well as all list.
147                         newItem[this._rootItemPropName]=true;
148                         this._arrayOfTopLevelItems.push(newItem);
149                 }
150                 
151                 this._pending._newItems[newIdentity] = newItem;
152                 
153                 //Clone over the properties to the new item
154                 for(var key in keywordArgs){
155                         if(key === this._storeRefPropName || key === this._itemNumPropName){
156                                 // Bummer, the user is trying to do something like
157                                 // newItem({_S:"foo"}).  Unfortunately, our superclass,
158                                 // ItemFileReadStore, is already using _S in each of our items
159                                 // to hold private info.  To avoid a naming collision, we 
160                                 // need to move all our private info to some other property 
161                                 // of all the items/objects.  So, we need to iterate over all
162                                 // the items and do something like: 
163                                 //    item.__S = item._S;
164                                 //    item._S = undefined;
165                                 // But first we have to make sure the new "__S" variable is 
166                                 // not in use, which means we have to iterate over all the 
167                                 // items checking for that.
168                                 throw new Error("encountered bug in ItemFileWriteStore.newItem");
169                         }
170                         var value = keywordArgs[key];
171                         if(!dojo.isArray(value)){
172                                 value = [value];
173                         }
174                         newItem[key] = value;
175                         if(this.referenceIntegrity){
176                                 for(var i = 0; i < value.length; i++){
177                                         var val = value[i];
178                                         if(this.isItem(val)){
179                                                 this._addReferenceToMap(val, newItem, key);
180                                         }
181                                 }
182                         }
183                 }
184                 this.onNew(newItem, pInfo); // dojo.data.api.Notification call
185                 return newItem; // item
186         },
187         
188         _removeArrayElement: function(/* Array */ array, /* anything */ element){
189                 var index = dojo.indexOf(array, element);
190                 if (index != -1){
191                         array.splice(index, 1);
192                         return true;
193                 }
194                 return false;
195         },
196         
197         deleteItem: function(/* item */ item){
198                 // summary: See dojo.data.api.Write.deleteItem()
199                 this._assert(!this._saveInProgress);
200                 this._assertIsItem(item);
201
202                 // Remove this item from the _arrayOfAllItems, but leave a null value in place
203                 // of the item, so as not to change the length of the array, so that in newItem() 
204                 // we can still safely do: newIdentity = this._arrayOfAllItems.length;
205                 var indexInArrayOfAllItems = item[this._itemNumPropName];
206                 var identity = this.getIdentity(item);
207
208                 //If we have reference integrity on, we need to do reference cleanup for the deleted item
209                 if(this.referenceIntegrity){
210                         //First scan all the attributes of this items for references and clean them up in the map 
211                         //As this item is going away, no need to track its references anymore.
212
213                         //Get the attributes list before we generate the backup so it 
214                         //doesn't pollute the attributes list.
215                         var attributes = this.getAttributes(item);
216
217                         //Backup the map, we'll have to restore it potentially, in a revert.
218                         if(item[this._reverseRefMap]){
219                                 item["backup_" + this._reverseRefMap] = dojo.clone(item[this._reverseRefMap]);
220                         }
221                         
222                         //TODO:  This causes a reversion problem.  This list won't be restored on revert since it is
223                         //attached to the 'value'. item, not ours.  Need to back tese up somehow too.
224                         //Maybe build a map of the backup of the entries and attach it to the deleted item to be restored
225                         //later.  Or just record them and call _addReferenceToMap on them in revert.
226                         dojo.forEach(attributes, function(attribute){
227                                 dojo.forEach(this.getValues(item, attribute), function(value){
228                                         if(this.isItem(value)){
229                                                 //We have to back up all the references we had to others so they can be restored on a revert.
230                                                 if(!item["backupRefs_" + this._reverseRefMap]){
231                                                         item["backupRefs_" + this._reverseRefMap] = [];
232                                                 }
233                                                 item["backupRefs_" + this._reverseRefMap].push({id: this.getIdentity(value), attr: attribute});
234                                                 this._removeReferenceFromMap(value, item, attribute);
235                                         }
236                                 }, this);
237                         }, this);
238
239                         //Next, see if we have references to this item, if we do, we have to clean them up too.
240                         var references = item[this._reverseRefMap];
241                         if(references){
242                                 //Look through all the items noted as references to clean them up.
243                                 for(var itemId in references){
244                                         var containingItem = null;
245                                         if(this._itemsByIdentity){
246                                                 containingItem = this._itemsByIdentity[itemId];
247                                         }else{
248                                                 containingItem = this._arrayOfAllItems[itemId];
249                                         }
250                                         //We have a reference to a containing item, now we have to process the
251                                         //attributes and clear all references to the item being deleted.
252                                         if(containingItem){
253                                                 for(var attribute in references[itemId]){
254                                                         var oldValues = this.getValues(containingItem, attribute) || [];
255                                                         var newValues = dojo.filter(oldValues, function(possibleItem){
256                                                            return !(this.isItem(possibleItem) && this.getIdentity(possibleItem) == identity);
257                                                         }, this);
258                                                         //Remove the note of the reference to the item and set the values on the modified attribute.
259                                                         this._removeReferenceFromMap(item, containingItem, attribute); 
260                                                         if(newValues.length < oldValues.length){
261                                                                 this.setValues(containingItem, attribute, newValues);
262                                                         }
263                                                 }
264                                         }
265                                 }
266                         }
267                 }
268
269                 this._arrayOfAllItems[indexInArrayOfAllItems] = null;
270
271                 item[this._storeRefPropName] = null;
272                 if(this._itemsByIdentity){
273                         delete this._itemsByIdentity[identity];
274                 }
275                 this._pending._deletedItems[identity] = item;
276                 
277                 //Remove from the toplevel items, if necessary...
278                 if(item[this._rootItemPropName]){
279                         this._removeArrayElement(this._arrayOfTopLevelItems, item);
280                 }
281                 this.onDelete(item); // dojo.data.api.Notification call
282                 return true;
283         },
284
285         setValue: function(/* item */ item, /* attribute-name-string */ attribute, /* almost anything */ value){
286                 // summary: See dojo.data.api.Write.set()
287                 return this._setValueOrValues(item, attribute, value, true); // boolean
288         },
289         
290         setValues: function(/* item */ item, /* attribute-name-string */ attribute, /* array */ values){
291                 // summary: See dojo.data.api.Write.setValues()
292                 return this._setValueOrValues(item, attribute, values, true); // boolean
293         },
294         
295         unsetAttribute: function(/* item */ item, /* attribute-name-string */ attribute){
296                 // summary: See dojo.data.api.Write.unsetAttribute()
297                 return this._setValueOrValues(item, attribute, [], true);
298         },
299         
300         _setValueOrValues: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ newValueOrValues, /*boolean?*/ callOnSet){
301                 this._assert(!this._saveInProgress);
302                 
303                 // Check for valid arguments
304                 this._assertIsItem(item);
305                 this._assert(dojo.isString(attribute));
306                 this._assert(typeof newValueOrValues !== "undefined");
307
308                 // Make sure the user isn't trying to change the item's identity
309                 var identifierAttribute = this._getIdentifierAttribute();
310                 if(attribute == identifierAttribute){
311                         throw new Error("ItemFileWriteStore does not have support for changing the value of an item's identifier.");
312                 }
313
314                 // To implement the Notification API, we need to make a note of what
315                 // the old attribute value was, so that we can pass that info when
316                 // we call the onSet method.
317                 var oldValueOrValues = this._getValueOrValues(item, attribute);
318
319                 var identity = this.getIdentity(item);
320                 if(!this._pending._modifiedItems[identity]){
321                         // Before we actually change the item, we make a copy of it to 
322                         // record the original state, so that we'll be able to revert if 
323                         // the revert method gets called.  If the item has already been
324                         // modified then there's no need to do this now, since we already
325                         // have a record of the original state.                                         
326                         var copyOfItemState = {};
327                         for(var key in item){
328                                 if((key === this._storeRefPropName) || (key === this._itemNumPropName) || (key === this._rootItemPropName)){
329                                         copyOfItemState[key] = item[key];
330                                 }else if(key === this._reverseRefMap){
331                                         copyOfItemState[key] = dojo.clone(item[key]);
332                                 }else{
333                                         copyOfItemState[key] = item[key].slice(0, item[key].length);
334                                 }
335                         }
336                         // Now mark the item as dirty, and save the copy of the original state
337                         this._pending._modifiedItems[identity] = copyOfItemState;
338                 }
339                 
340                 // Okay, now we can actually change this attribute on the item
341                 var success = false;
342                 
343                 if(dojo.isArray(newValueOrValues) && newValueOrValues.length === 0){
344                         
345                         // If we were passed an empty array as the value, that counts
346                         // as "unsetting" the attribute, so we need to remove this 
347                         // attribute from the item.
348                         success = delete item[attribute];
349                         newValueOrValues = undefined; // used in the onSet Notification call below
350
351                         if(this.referenceIntegrity && oldValueOrValues){
352                                 var oldValues = oldValueOrValues;
353                                 if (!dojo.isArray(oldValues)){
354                                         oldValues = [oldValues];
355                                 }
356                                 for(var i = 0; i < oldValues.length; i++){
357                                         var value = oldValues[i];
358                                         if(this.isItem(value)){
359                                                 this._removeReferenceFromMap(value, item, attribute);
360                                         }
361                                 }
362                         }
363                 }else{
364                         var newValueArray;
365                         if(dojo.isArray(newValueOrValues)){
366                                 var newValues = newValueOrValues;
367                                 // Unfortunately, it's not safe to just do this:
368                                 //    newValueArray = newValues;
369                                 // Instead, we need to copy the array, which slice() does very nicely.
370                                 // This is so that our internal data structure won't  
371                                 // get corrupted if the user mucks with the values array *after*
372                                 // calling setValues().
373                                 newValueArray = newValueOrValues.slice(0, newValueOrValues.length);
374                         }else{
375                                 newValueArray = [newValueOrValues];
376                         }
377
378                         //We need to handle reference integrity if this is on. 
379                         //In the case of set, we need to see if references were added or removed
380                         //and update the reference tracking map accordingly.
381                         if(this.referenceIntegrity){
382                                 if(oldValueOrValues){
383                                         var oldValues = oldValueOrValues;
384                                         if(!dojo.isArray(oldValues)){
385                                                 oldValues = [oldValues];
386                                         }
387                                         //Use an associative map to determine what was added/removed from the list.
388                                         //Should be O(n) performant.  First look at all the old values and make a list of them
389                                         //Then for any item not in the old list, we add it.  If it was already present, we remove it.
390                                         //Then we pass over the map and any references left it it need to be removed (IE, no match in
391                                         //the new values list).
392                                         var map = {};
393                                         dojo.forEach(oldValues, function(possibleItem){
394                                                 if(this.isItem(possibleItem)){
395                                                         var id = this.getIdentity(possibleItem);
396                                                         map[id.toString()] = true;
397                                                 }
398                                         }, this);
399                                         dojo.forEach(newValueArray, function(possibleItem){
400                                                 if(this.isItem(possibleItem)){
401                                                         var id = this.getIdentity(possibleItem);
402                                                         if(map[id.toString()]){
403                                                                 delete map[id.toString()];
404                                                         }else{
405                                                                 this._addReferenceToMap(possibleItem, item, attribute); 
406                                                         }
407                                                 }
408                                         }, this);
409                                         for(var rId in map){
410                                                 var removedItem;
411                                                 if(this._itemsByIdentity){
412                                                         removedItem = this._itemsByIdentity[rId];
413                                                 }else{
414                                                         removedItem = this._arrayOfAllItems[rId];
415                                                 }
416                                                 this._removeReferenceFromMap(removedItem, item, attribute);
417                                         }
418                                 }else{
419                                         //Everything is new (no old values) so we have to just
420                                         //insert all the references, if any.
421                                         for(var i = 0; i < newValueArray.length; i++){
422                                                 var value = newValueArray[i];
423                                                 if(this.isItem(value)){
424                                                         this._addReferenceToMap(value, item, attribute);
425                                                 }
426                                         }
427                                 }
428                         }
429                         item[attribute] = newValueArray;
430                         success = true;
431                 }
432
433                 // Now we make the dojo.data.api.Notification call
434                 if(callOnSet){
435                         this.onSet(item, attribute, oldValueOrValues, newValueOrValues); 
436                 }
437                 return success; // boolean
438         },
439
440         _addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){
441                 //      summary:
442                 //              Method to add an reference map entry for an item and attribute.
443                 //      description:
444                 //              Method to add an reference map entry for an item and attribute.                  //
445                 //      refItem:
446                 //              The item that is referenced.
447                 //      parentItem:
448                 //              The item that holds the new reference to refItem.
449                 //      attribute:
450                 //              The attribute on parentItem that contains the new reference.
451                  
452                 var parentId = this.getIdentity(parentItem);
453                 var references = refItem[this._reverseRefMap];
454
455                 if(!references){
456                         references = refItem[this._reverseRefMap] = {};
457                 }
458                 var itemRef = references[parentId];
459                 if(!itemRef){
460                         itemRef = references[parentId] = {};
461                 }
462                 itemRef[attribute] = true;
463         },
464
465         _removeReferenceFromMap: function(/* item */ refItem, /* item */ parentItem, /*strin*/ attribute){
466                 //      summary:
467                 //              Method to remove an reference map entry for an item and attribute.
468                 //      description:
469                 //              Method to remove an reference map entry for an item and attribute.  This will
470                 //              also perform cleanup on the map such that if there are no more references at all to 
471                 //              the item, its reference object and entry are removed.
472                 //
473                 //      refItem:
474                 //              The item that is referenced.
475                 //      parentItem:
476                 //              The item holding a reference to refItem.
477                 //      attribute:
478                 //              The attribute on parentItem that contains the reference.
479                 var identity = this.getIdentity(parentItem);
480                 var references = refItem[this._reverseRefMap];
481                 var itemId;
482                 if(references){
483                         for(itemId in references){
484                                 if(itemId == identity){
485                                         delete references[itemId][attribute];
486                                         if(this._isEmpty(references[itemId])){
487                                                 delete references[itemId];
488                                         }
489                                 }
490                         }
491                         if(this._isEmpty(references)){
492                                 delete refItem[this._reverseRefMap];
493                         }
494                 }
495         },
496
497         _dumpReferenceMap: function(){
498                 //      summary:
499                 //              Function to dump the reverse reference map of all items in the store for debug purposes.
500                 //      description:
501                 //              Function to dump the reverse reference map of all items in the store for debug purposes.
502                 var i;
503                 for(i = 0; i < this._arrayOfAllItems.length; i++){
504                         var item = this._arrayOfAllItems[i];
505                         if(item && item[this._reverseRefMap]){
506                                 console.log("Item: [" + this.getIdentity(item) + "] is referenced by: " + dojo.toJson(item[this._reverseRefMap]));
507                         }
508                 }
509         },
510                                                    
511         _getValueOrValues: function(/* item */ item, /* attribute-name-string */ attribute){
512                 var valueOrValues = undefined;
513                 if(this.hasAttribute(item, attribute)){
514                         var valueArray = this.getValues(item, attribute);
515                         if(valueArray.length == 1){
516                                 valueOrValues = valueArray[0];
517                         }else{
518                                 valueOrValues = valueArray;
519                         }
520                 }
521                 return valueOrValues;
522         },
523         
524         _flatten: function(/* anything */ value){
525                 if(this.isItem(value)){
526                         var item = value;
527                         // Given an item, return an serializable object that provides a 
528                         // reference to the item.
529                         // For example, given kermit:
530                         //    var kermit = store.newItem({id:2, name:"Kermit"});
531                         // we want to return
532                         //    {_reference:2}
533                         var identity = this.getIdentity(item);
534                         var referenceObject = {_reference: identity};
535                         return referenceObject;
536                 }else{
537                         if(typeof value === "object"){
538                                 for(var type in this._datatypeMap){
539                                         var typeMap = this._datatypeMap[type];
540                                         if (dojo.isObject(typeMap) && !dojo.isFunction(typeMap)){
541                                                 if(value instanceof typeMap.type){
542                                                         if(!typeMap.serialize){
543                                                                 throw new Error("ItemFileWriteStore:  No serializer defined for type mapping: [" + type + "]");
544                                                         }
545                                                         return {_type: type, _value: typeMap.serialize(value)};
546                                                 }
547                                         } else if(value instanceof typeMap){
548                                                 //SImple mapping, therefore, return as a toString serialization.
549                                                 return {_type: type, _value: value.toString()};
550                                         }
551                                 }
552                         }
553                         return value;
554                 }
555         },
556         
557         _getNewFileContentString: function(){
558                 // summary: 
559                 //              Generate a string that can be saved to a file.
560                 //              The result should look similar to:
561                 //              http://trac.dojotoolkit.org/browser/dojo/trunk/tests/data/countries.json
562                 var serializableStructure = {};
563                 
564                 var identifierAttribute = this._getIdentifierAttribute();
565                 if(identifierAttribute !== Number){
566                         serializableStructure.identifier = identifierAttribute;
567                 }
568                 if(this._labelAttr){
569                         serializableStructure.label = this._labelAttr;
570                 }
571                 serializableStructure.items = [];
572                 for(var i = 0; i < this._arrayOfAllItems.length; ++i){
573                         var item = this._arrayOfAllItems[i];
574                         if(item !== null){
575                                 var serializableItem = {};
576                                 for(var key in item){
577                                         if(key !== this._storeRefPropName && key !== this._itemNumPropName){
578                                                 var attribute = key;
579                                                 var valueArray = this.getValues(item, attribute);
580                                                 if(valueArray.length == 1){
581                                                         serializableItem[attribute] = this._flatten(valueArray[0]);
582                                                 }else{
583                                                         var serializableArray = [];
584                                                         for(var j = 0; j < valueArray.length; ++j){
585                                                                 serializableArray.push(this._flatten(valueArray[j]));
586                                                                 serializableItem[attribute] = serializableArray;
587                                                         }
588                                                 }
589                                         }
590                                 }
591                                 serializableStructure.items.push(serializableItem);
592                         }
593                 }
594                 var prettyPrint = true;
595                 return dojo.toJson(serializableStructure, prettyPrint);
596         },
597
598         _isEmpty: function(something){
599                 //      summary: 
600                 //              Function to determine if an array or object has no properties or values.
601                 //      something:
602                 //              The array or object to examine.
603                 var empty = true;
604                 if(dojo.isObject(something)){
605                         var i;
606                         for(i in something){
607                                 empty = false;
608                                 break;
609                         }
610                 }else if(dojo.isArray(something)){
611                         if(something.length > 0){
612                                 empty = false;
613                         }
614                 }
615                 return empty; //boolean
616         },
617         
618         save: function(/* object */ keywordArgs){
619                 // summary: See dojo.data.api.Write.save()
620                 this._assert(!this._saveInProgress);
621                 
622                 // this._saveInProgress is set to true, briefly, from when save is first called to when it completes
623                 this._saveInProgress = true;
624                 
625                 var self = this;
626                 var saveCompleteCallback = function(){
627                         self._pending = {
628                                 _newItems:{}, 
629                                 _modifiedItems:{},
630                                 _deletedItems:{}
631                         };
632
633                         self._saveInProgress = false; // must come after this._pending is cleared, but before any callbacks
634                         if(keywordArgs && keywordArgs.onComplete){
635                                 var scope = keywordArgs.scope || dojo.global;
636                                 keywordArgs.onComplete.call(scope);
637                         }
638                 };
639                 var saveFailedCallback = function(){
640                         self._saveInProgress = false;
641                         if(keywordArgs && keywordArgs.onError){
642                                 var scope = keywordArgs.scope || dojo.global;
643                                 keywordArgs.onError.call(scope);
644                         }
645                 };
646                 
647                 if(this._saveEverything){
648                         var newFileContentString = this._getNewFileContentString();
649                         this._saveEverything(saveCompleteCallback, saveFailedCallback, newFileContentString);
650                 }
651                 if(this._saveCustom){
652                         this._saveCustom(saveCompleteCallback, saveFailedCallback);
653                 }
654                 if(!this._saveEverything && !this._saveCustom){
655                         // Looks like there is no user-defined save-handler function.
656                         // That's fine, it just means the datastore is acting as a "mock-write"
657                         // store -- changes get saved in memory but don't get saved to disk.
658                         saveCompleteCallback();
659                 }
660         },
661         
662         revert: function(){
663                 // summary: See dojo.data.api.Write.revert()
664                 this._assert(!this._saveInProgress);
665
666                 var identity;
667                 for(identity in this._pending._newItems){
668                         var newItem = this._pending._newItems[identity];
669                         newItem[this._storeRefPropName] = null;
670                         // null out the new item, but don't change the array index so
671                         // so we can keep using _arrayOfAllItems.length.
672                         this._arrayOfAllItems[newItem[this._itemNumPropName]] = null;
673                         if(newItem[this._rootItemPropName]){
674                                 this._removeArrayElement(this._arrayOfTopLevelItems, newItem);
675                         }
676                         if(this._itemsByIdentity){
677                                 delete this._itemsByIdentity[identity];
678                         }
679                 }
680                 for(identity in this._pending._modifiedItems){
681                         // find the original item and the modified item that replaced it
682                         var originalItem = this._pending._modifiedItems[identity];
683                         var modifiedItem = null;
684                         if(this._itemsByIdentity){
685                                 modifiedItem = this._itemsByIdentity[identity];
686                         }else{
687                                 modifiedItem = this._arrayOfAllItems[identity];
688                         }
689                         
690                         // make the original item into a full-fledged item again
691                         originalItem[this._storeRefPropName] = this;
692                         modifiedItem[this._storeRefPropName] = null;
693
694                         // replace the modified item with the original one
695                         var arrayIndex = modifiedItem[this._itemNumPropName];
696                         this._arrayOfAllItems[arrayIndex] = originalItem;
697                         
698                         if(modifiedItem[this._rootItemPropName]){
699                                 var i;
700                                 for (i = 0; i < this._arrayOfTopLevelItems.length; i++) {
701                                         var possibleMatch = this._arrayOfTopLevelItems[i];
702                                         if (this.getIdentity(possibleMatch) == identity){
703                                                 this._arrayOfTopLevelItems[i] = originalItem;
704                                                 break;
705                                         }
706                                 }
707                         }
708                         if(this._itemsByIdentity){
709                                 this._itemsByIdentity[identity] = originalItem;
710                         }                       
711                 }
712                 var deletedItem;
713                 for(identity in this._pending._deletedItems){
714                         deletedItem = this._pending._deletedItems[identity];
715                         deletedItem[this._storeRefPropName] = this;
716                         var index = deletedItem[this._itemNumPropName];
717
718                         //Restore the reverse refererence map, if any.
719                         if(deletedItem["backup_" + this._reverseRefMap]){
720                                 deletedItem[this._reverseRefMap] = deletedItem["backup_" + this._reverseRefMap];
721                                 delete deletedItem["backup_" + this._reverseRefMap];
722                         }
723                         this._arrayOfAllItems[index] = deletedItem;
724                         if (this._itemsByIdentity) {
725                                 this._itemsByIdentity[identity] = deletedItem;
726                         }
727                         if(deletedItem[this._rootItemPropName]){
728                                 this._arrayOfTopLevelItems.push(deletedItem);
729                         }         
730                 }
731                 //We have to pass through it again and restore the reference maps after all the
732                 //undeletes have occurred.
733                 for(identity in this._pending._deletedItems){
734                         deletedItem = this._pending._deletedItems[identity];
735                         if(deletedItem["backupRefs_" + this._reverseRefMap]){
736                                 dojo.forEach(deletedItem["backupRefs_" + this._reverseRefMap], function(reference){
737                                         var refItem;
738                                         if(this._itemsByIdentity){
739                                                 refItem = this._itemsByIdentity[reference.id];
740                                         }else{
741                                                 refItem = this._arrayOfAllItems[reference.id];
742                                         }
743                                         this._addReferenceToMap(refItem, deletedItem, reference.attr);
744                                 }, this);
745                                 delete deletedItem["backupRefs_" + this._reverseRefMap]; 
746                         }
747                 }
748
749                 this._pending = {
750                         _newItems:{}, 
751                         _modifiedItems:{}, 
752                         _deletedItems:{}
753                 };
754                 return true; // boolean
755         },
756         
757         isDirty: function(/* item? */ item){
758                 // summary: See dojo.data.api.Write.isDirty()
759                 if(item){
760                         // return true if the item is dirty
761                         var identity = this.getIdentity(item);
762                         return new Boolean(this._pending._newItems[identity] || 
763                                 this._pending._modifiedItems[identity] ||
764                                 this._pending._deletedItems[identity]); // boolean
765                 }else{
766                         // return true if the store is dirty -- which means return true
767                         // if there are any new items, dirty items, or modified items
768                         if(!this._isEmpty(this._pending._newItems) || 
769                            !this._isEmpty(this._pending._modifiedItems) ||
770                            !this._isEmpty(this._pending._deletedItems)){
771                                 return true;
772                         }
773                         return false; // boolean
774                 }
775         },
776
777 /* dojo.data.api.Notification */
778
779         onSet: function(/* item */ item, 
780                                         /*attribute-name-string*/ attribute, 
781                                         /*object | array*/ oldValue,
782                                         /*object | array*/ newValue){
783                 // summary: See dojo.data.api.Notification.onSet()
784                 
785                 // No need to do anything. This method is here just so that the 
786                 // client code can connect observers to it.
787         },
788
789         onNew: function(/* item */ newItem, /*object?*/ parentInfo){
790                 // summary: See dojo.data.api.Notification.onNew()
791                 
792                 // No need to do anything. This method is here just so that the 
793                 // client code can connect observers to it. 
794         },
795
796         onDelete: function(/* item */ deletedItem){
797                 // summary: See dojo.data.api.Notification.onDelete()
798                 
799                 // No need to do anything. This method is here just so that the 
800                 // client code can connect observers to it. 
801         }
802 });
803
804 }