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