source: Dev/branches/rest-dojo-ui/client/dojo/data/ItemFileWriteStore.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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