source: Dev/branches/rest-dojo-ui/client/dojox/data/AndOrWriteStore.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).

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