ItemFileWriteStore.js 29 KB

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