source: Dev/trunk/src/client/dojo/data/ItemFileWriteStore.js @ 485

Last change on this file since 485 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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