AndOrReadStore.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046
  1. define("dojox/data/AndOrReadStore", ["dojo/_base/kernel", "dojo/_base/declare", "dojo/_base/lang", "dojo/data/util/filter", "dojo/data/util/simpleFetch",
  2. "dojo/_base/array", "dojo/date/stamp", "dojo/_base/json", "dojo/_base/window", "dojo/_base/xhr"],
  3. function(kernel, declare, lang, filterUtil, simpleFetch, array, dateStamp, json, winUtil, xhr) {
  4. var AndOrReadStore = declare("dojox.data.AndOrReadStore", null, {
  5. // summary:
  6. // AndOrReadStore uses ItemFileReadStore as a base, modifying only the query (_fetchItems) section.
  7. // Supports queries of the form: query:"id:1* OR dept:'Sales Department' || (id:2* && NOT dept:S*)"
  8. // Includes legacy/widget support via:
  9. // query:{complexQuery:"id:1* OR dept:'Sales Department' || (id:2* && NOT dept:S*)"}
  10. // The ItemFileReadStore implements the dojo.data.api.Read API and reads
  11. // data from JSON files that have contents in this format --
  12. // { items: [
  13. // { name:'Kermit', color:'green', age:12, friends:['Gonzo', {_reference:{name:'Fozzie Bear'}}]},
  14. // { name:'Fozzie Bear', wears:['hat', 'tie']},
  15. // { name:'Miss Piggy', pets:'Foo-Foo'}
  16. // ]}
  17. // Note that it can also contain an 'identifer' property that specified which attribute on the items
  18. // in the array of items that acts as the unique identifier for that item.
  19. //
  20. constructor: function(/* Object */ keywordParameters){
  21. // summary: constructor
  22. // keywordParameters: {url: String}
  23. // keywordParameters: {data: jsonObject}
  24. // keywordParameters: {typeMap: object)
  25. // The structure of the typeMap object is as follows:
  26. // {
  27. // type0: function || object,
  28. // type1: function || object,
  29. // ...
  30. // typeN: function || object
  31. // }
  32. // Where if it is a function, it is assumed to be an object constructor that takes the
  33. // value of _value as the initialization parameters. If it is an object, then it is assumed
  34. // to be an object of general form:
  35. // {
  36. // type: function, //constructor.
  37. // deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately.
  38. // }
  39. this._arrayOfAllItems = [];
  40. this._arrayOfTopLevelItems = [];
  41. this._loadFinished = false;
  42. this._jsonFileUrl = keywordParameters.url;
  43. this._ccUrl = keywordParameters.url;
  44. this.url = keywordParameters.url;
  45. this._jsonData = keywordParameters.data;
  46. this.data = null;
  47. this._datatypeMap = keywordParameters.typeMap || {};
  48. if(!this._datatypeMap['Date']){
  49. //If no default mapping for dates, then set this as default.
  50. //We use the dojo.date.stamp here because the ISO format is the 'dojo way'
  51. //of generically representing dates.
  52. this._datatypeMap['Date'] = {
  53. type: Date,
  54. deserialize: function(value){
  55. return dateStamp.fromISOString(value);
  56. }
  57. };
  58. }
  59. this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true};
  60. this._itemsByIdentity = null;
  61. this._storeRefPropName = "_S"; // Default name for the store reference to attach to every item.
  62. this._itemNumPropName = "_0"; // Default Item Id for isItem to attach to every item.
  63. this._rootItemPropName = "_RI"; // Default Item Id for isItem to attach to every item.
  64. this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity
  65. this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset.
  66. this._queuedFetches = [];
  67. if(keywordParameters.urlPreventCache !== undefined){
  68. this.urlPreventCache = keywordParameters.urlPreventCache?true:false;
  69. }
  70. if(keywordParameters.hierarchical !== undefined){
  71. this.hierarchical = keywordParameters.hierarchical?true:false;
  72. }
  73. if(keywordParameters.clearOnClose){
  74. this.clearOnClose = true;
  75. }
  76. },
  77. url: "", // use "" rather than undefined for the benefit of the parser (#3539)
  78. //Internal var, crossCheckUrl. Used so that setting either url or _jsonFileUrl, can still trigger a reload
  79. //when clearOnClose and close is used.
  80. _ccUrl: "",
  81. data: null, //Make this parser settable.
  82. typeMap: null, //Make this parser settable.
  83. //Parameter to allow users to specify if a close call should force a reload or not.
  84. //By default, it retains the old behavior of not clearing if close is called. But
  85. //if set true, the store will be reset to default state. Note that by doing this,
  86. //all item handles will become invalid and a new fetch must be issued.
  87. clearOnClose: false,
  88. //Parameter to allow specifying if preventCache should be passed to the xhrGet call or not when loading data from a url.
  89. //Note this does not mean the store calls the server on each fetch, only that the data load has preventCache set as an option.
  90. //Added for tracker: #6072
  91. urlPreventCache: false,
  92. //Parameter to indicate to process data from the url as hierarchical
  93. //(data items can contain other data items in js form). Default is true
  94. //for backwards compatibility. False means only root items are processed
  95. //as items, all child objects outside of type-mapped objects and those in
  96. //specific reference format, are left straight JS data objects.
  97. hierarchical: true,
  98. _assertIsItem: function(/* item */ item){
  99. // summary:
  100. // This function tests whether the item passed in is indeed an item in the store.
  101. // item:
  102. // The item to test for being contained by the store.
  103. if(!this.isItem(item)){
  104. throw new Error("dojox.data.AndOrReadStore: Invalid item argument.");
  105. }
  106. },
  107. _assertIsAttribute: function(/* attribute-name-string */ attribute){
  108. // summary:
  109. // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
  110. // attribute:
  111. // The attribute to test for being contained by the store.
  112. if(typeof attribute !== "string"){
  113. throw new Error("dojox.data.AndOrReadStore: Invalid attribute argument.");
  114. }
  115. },
  116. getValue: function( /* item */ item,
  117. /* attribute-name-string */ attribute,
  118. /* value? */ defaultValue){
  119. // summary:
  120. // See dojo.data.api.Read.getValue()
  121. var values = this.getValues(item, attribute);
  122. return (values.length > 0)?values[0]:defaultValue; // mixed
  123. },
  124. getValues: function(/* item */ item,
  125. /* attribute-name-string */ attribute){
  126. // summary:
  127. // See dojo.data.api.Read.getValues()
  128. this._assertIsItem(item);
  129. this._assertIsAttribute(attribute);
  130. var arr = item[attribute] || [];
  131. // Clone it before returning. refs: #10474
  132. return arr.slice(0, arr.length); // Array
  133. },
  134. getAttributes: function(/* item */ item){
  135. // summary:
  136. // See dojo.data.api.Read.getAttributes()
  137. this._assertIsItem(item);
  138. var attributes = [];
  139. for(var key in item){
  140. // Save off only the real item attributes, not the special id marks for O(1) isItem.
  141. if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){
  142. attributes.push(key);
  143. }
  144. }
  145. return attributes; // Array
  146. },
  147. hasAttribute: function( /* item */ item,
  148. /* attribute-name-string */ attribute){
  149. // summary:
  150. // See dojo.data.api.Read.hasAttribute()
  151. this._assertIsItem(item);
  152. this._assertIsAttribute(attribute);
  153. return (attribute in item);
  154. },
  155. containsValue: function(/* item */ item,
  156. /* attribute-name-string */ attribute,
  157. /* anything */ value){
  158. // summary:
  159. // See dojo.data.api.Read.containsValue()
  160. var regexp = undefined;
  161. if(typeof value === "string"){
  162. regexp = filterUtil.patternToRegExp(value, false);
  163. }
  164. return this._containsValue(item, attribute, value, regexp); //boolean.
  165. },
  166. _containsValue: function( /* item */ item,
  167. /* attribute-name-string */ attribute,
  168. /* anything */ value,
  169. /* RegExp?*/ regexp){
  170. // summary:
  171. // Internal function for looking at the values contained by the item.
  172. // description:
  173. // Internal function for looking at the values contained by the item. This
  174. // function allows for denoting if the comparison should be case sensitive for
  175. // strings or not (for handling filtering cases where string case should not matter)
  176. //
  177. // item:
  178. // The data item to examine for attribute values.
  179. // attribute:
  180. // The attribute to inspect.
  181. // value:
  182. // The value to match.
  183. // regexp:
  184. // Optional regular expression generated off value if value was of string type to handle wildcarding.
  185. // If present and attribute values are string, then it can be used for comparison instead of 'value'
  186. return array.some(this.getValues(item, attribute), function(possibleValue){
  187. if(possibleValue !== null && !lang.isObject(possibleValue) && regexp){
  188. if(possibleValue.toString().match(regexp)){
  189. return true; // Boolean
  190. }
  191. } else if(value === possibleValue){
  192. return true; // Boolean
  193. } else {
  194. return false;
  195. }
  196. });
  197. },
  198. isItem: function(/* anything */ something){
  199. // summary:
  200. // See dojo.data.api.Read.isItem()
  201. if(something && something[this._storeRefPropName] === this){
  202. if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){
  203. return true;
  204. }
  205. }
  206. return false; // Boolean
  207. },
  208. isItemLoaded: function(/* anything */ something){
  209. // summary:
  210. // See dojo.data.api.Read.isItemLoaded()
  211. return this.isItem(something); //boolean
  212. },
  213. loadItem: function(/* object */ keywordArgs){
  214. // summary:
  215. // See dojo.data.api.Read.loadItem()
  216. this._assertIsItem(keywordArgs.item);
  217. },
  218. getFeatures: function(){
  219. // summary:
  220. // See dojo.data.api.Read.getFeatures()
  221. return this._features; //Object
  222. },
  223. getLabel: function(/* item */ item){
  224. // summary:
  225. // See dojo.data.api.Read.getLabel()
  226. if(this._labelAttr && this.isItem(item)){
  227. return this.getValue(item,this._labelAttr); //String
  228. }
  229. return undefined; //undefined
  230. },
  231. getLabelAttributes: function(/* item */ item){
  232. // summary:
  233. // See dojo.data.api.Read.getLabelAttributes()
  234. if(this._labelAttr){
  235. return [this._labelAttr]; //array
  236. }
  237. return null; //null
  238. },
  239. _fetchItems: function( /* Object */ keywordArgs,
  240. /* Function */ findCallback,
  241. /* Function */ errorCallback){
  242. // summary:
  243. // See dojo.data.util.simpleFetch.fetch()
  244. // filter modified to permit complex queries where
  245. // logical operators are case insensitive:
  246. // , NOT AND OR ( ) ! && ||
  247. // Note: "," included for quoted/string legacy queries.
  248. var self = this;
  249. var filter = function(requestArgs, arrayOfItems){
  250. var items = [];
  251. if(requestArgs.query){
  252. //Complete copy, we may have to mess with it.
  253. //Safer than clone, which does a shallow copy, I believe.
  254. var query = json.fromJson(json.toJson(requestArgs.query));
  255. //Okay, object form query, we have to check to see if someone mixed query methods (such as using FilteringSelect
  256. //with a complexQuery). In that case, the params need to be anded to the complex query statement.
  257. //See defect #7980
  258. if(typeof query == "object" ){
  259. var count = 0;
  260. var p;
  261. for(p in query){
  262. count++;
  263. }
  264. if(count > 1 && query.complexQuery){
  265. var cq = query.complexQuery;
  266. var wrapped = false;
  267. for(p in query){
  268. if(p !== "complexQuery"){
  269. //We should wrap this in () as it should and with the entire complex query
  270. //Not just part of it.
  271. if(!wrapped){
  272. cq = "( " + cq + " )";
  273. wrapped = true;
  274. }
  275. //Make sure strings are quoted when going into complexQuery merge.
  276. var v = requestArgs.query[p];
  277. if(lang.isString(v)){
  278. v = "'" + v + "'";
  279. }
  280. cq += " AND " + p + ":" + v;
  281. delete query[p];
  282. }
  283. }
  284. query.complexQuery = cq;
  285. }
  286. }
  287. var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false;
  288. //for complex queries only: pattern = query[:|=]"NOT id:23* AND (type:'test*' OR dept:'bob') && !filed:true"
  289. //logical operators are case insensitive: , NOT AND OR ( ) ! && || // "," included for quoted/string legacy queries.
  290. if(typeof query != "string"){
  291. query = json.toJson(query);
  292. query = query.replace(/\\\\/g,"\\"); //counter toJson expansion of backslashes, e.g., foo\\*bar test.
  293. }
  294. query = query.replace(/\\"/g,"\""); //ditto, for embedded \" in lieu of " availability.
  295. var complexQuery = lang.trim(query.replace(/{|}/g,"")); //we can handle these, too.
  296. var pos2, i;
  297. if(complexQuery.match(/"? *complexQuery *"?:/)){ //case where widget required a json object, so use complexQuery:'the real query'
  298. complexQuery = lang.trim(complexQuery.replace(/"?\s*complexQuery\s*"?:/,""));
  299. var quotes = ["'",'"'];
  300. var pos1,colon;
  301. var flag = false;
  302. for(i = 0; i<quotes.length; i++){
  303. pos1 = complexQuery.indexOf(quotes[i]);
  304. pos2 = complexQuery.indexOf(quotes[i],1);
  305. colon = complexQuery.indexOf(":",1);
  306. if(pos1 === 0 && pos2 != -1 && colon < pos2){
  307. flag = true;
  308. break;
  309. } //first two sets of quotes don't occur before the first colon.
  310. }
  311. if(flag){ //dojo.toJson, and maybe user, adds surrounding quotes, which we need to remove.
  312. complexQuery = complexQuery.replace(/^\"|^\'|\"$|\'$/g,"");
  313. }
  314. } //end query="{complexQuery:'id:1* || dept:Sales'}" parsing (for when widget required json object query).
  315. var complexQuerySave = complexQuery;
  316. //valid logical operators.
  317. var begRegExp = /^,|^NOT |^AND |^OR |^\(|^\)|^!|^&&|^\|\|/i; //trailing space on some tokens on purpose.
  318. var sQuery = ""; //will be eval'ed for each i-th candidateItem, based on query components.
  319. var op = "";
  320. var val = "";
  321. var pos = -1;
  322. var err = false;
  323. var key = "";
  324. var value = "";
  325. var tok = "";
  326. pos2 = -1;
  327. for(i = 0; i < arrayOfItems.length; ++i){
  328. var match = true;
  329. var candidateItem = arrayOfItems[i];
  330. if(candidateItem === null){
  331. match = false;
  332. }else{
  333. //process entire string for this i-th candidateItem.
  334. complexQuery = complexQuerySave; //restore query for next candidateItem.
  335. sQuery = "";
  336. //work left to right, finding either key:value pair or logical operator at the beginning of the complexQuery string.
  337. //when found, concatenate to sQuery and remove from complexQuery and loop back.
  338. while(complexQuery.length > 0 && !err){
  339. op = complexQuery.match(begRegExp);
  340. //get/process/append one or two leading logical operators.
  341. while(op && !err){ //look for leading logical operators.
  342. complexQuery = lang.trim(complexQuery.replace(op[0],""));
  343. op = lang.trim(op[0]).toUpperCase();
  344. //convert some logical operators to their javascript equivalents for later eval.
  345. op = op == "NOT" ? "!" : op == "AND" || op == "," ? "&&" : op == "OR" ? "||" : op;
  346. op = " " + op + " ";
  347. sQuery += op;
  348. op = complexQuery.match(begRegExp);
  349. }//end op && !err
  350. //now get/process/append one key:value pair.
  351. if(complexQuery.length > 0){
  352. pos = complexQuery.indexOf(":");
  353. if(pos == -1){
  354. err = true;
  355. break;
  356. }else{
  357. key = lang.trim(complexQuery.substring(0,pos).replace(/\"|\'/g,""));
  358. complexQuery = lang.trim(complexQuery.substring(pos + 1));
  359. tok = complexQuery.match(/^\'|^\"/); //quoted?
  360. if(tok){
  361. tok = tok[0];
  362. pos = complexQuery.indexOf(tok);
  363. pos2 = complexQuery.indexOf(tok,pos + 1);
  364. if(pos2 == -1){
  365. err = true;
  366. break;
  367. }
  368. value = complexQuery.substring(pos + 1,pos2);
  369. if(pos2 == complexQuery.length - 1){ //quote is last character
  370. complexQuery = "";
  371. }else{
  372. complexQuery = lang.trim(complexQuery.substring(pos2 + 1));
  373. }
  374. sQuery += self._containsValue(candidateItem, key, value, filterUtil.patternToRegExp(value, ignoreCase));
  375. }
  376. else{ //not quoted, so a space, comma, or closing parens (or the end) will be the break.
  377. tok = complexQuery.match(/\s|\)|,/);
  378. if(tok){
  379. var pos3 = new Array(tok.length);
  380. for(var j = 0;j<tok.length;j++){
  381. pos3[j] = complexQuery.indexOf(tok[j]);
  382. }
  383. pos = pos3[0];
  384. if(pos3.length > 1){
  385. for(var j=1;j<pos3.length;j++){
  386. pos = Math.min(pos,pos3[j]);
  387. }
  388. }
  389. value = lang.trim(complexQuery.substring(0,pos));
  390. complexQuery = lang.trim(complexQuery.substring(pos));
  391. }else{ //not a space, so must be at the end of the complexQuery.
  392. value = lang.trim(complexQuery);
  393. complexQuery = "";
  394. } //end inner if(tok) else
  395. sQuery += self._containsValue(candidateItem, key, value, filterUtil.patternToRegExp(value, ignoreCase));
  396. } //end outer if(tok) else
  397. } //end found ":"
  398. } //end if(complexQuery.length > 0)
  399. } //end while complexQuery.length > 0 && !err, so finished the i-th item.
  400. match = eval(sQuery);
  401. } //end else is non-null candidateItem.
  402. if(match){
  403. items.push(candidateItem);
  404. }
  405. } //end for/next of all items.
  406. if(err){
  407. //soft fail.
  408. items = [];
  409. console.log("The store's _fetchItems failed, probably due to a syntax error in query.");
  410. }
  411. findCallback(items, requestArgs);
  412. }else{
  413. // No query...
  414. // We want a copy to pass back in case the parent wishes to sort the array.
  415. // We shouldn't allow resort of the internal list, so that multiple callers
  416. // can get lists and sort without affecting each other. We also need to
  417. // filter out any null values that have been left as a result of deleteItem()
  418. // calls in ItemFileWriteStore.
  419. for(var i = 0; i < arrayOfItems.length; ++i){
  420. var item = arrayOfItems[i];
  421. if(item !== null){
  422. items.push(item);
  423. }
  424. }
  425. findCallback(items, requestArgs);
  426. } //end if there is a query.
  427. }; //end filter function
  428. if(this._loadFinished){
  429. filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
  430. }else{
  431. if(this._jsonFileUrl !== this._ccUrl){
  432. kernel.deprecated("dojox.data.AndOrReadStore: ",
  433. "To change the url, set the url property of the store," +
  434. " not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0");
  435. this._ccUrl = this._jsonFileUrl;
  436. this.url = this._jsonFileUrl;
  437. }else if(this.url !== this._ccUrl){
  438. this._jsonFileUrl = this.url;
  439. this._ccUrl = this.url;
  440. }
  441. //See if there was any forced reset of data.
  442. if(this.data != null && this._jsonData == null){
  443. this._jsonData = this.data;
  444. this.data = null;
  445. }
  446. if(this._jsonFileUrl){
  447. //If fetches come in before the loading has finished, but while
  448. //a load is in progress, we have to defer the fetching to be
  449. //invoked in the callback.
  450. if(this._loadInProgress){
  451. this._queuedFetches.push({args: keywordArgs, filter: filter});
  452. }else{
  453. this._loadInProgress = true;
  454. var getArgs = {
  455. url: self._jsonFileUrl,
  456. handleAs: "json-comment-optional",
  457. preventCache: this.urlPreventCache
  458. };
  459. var getHandler = xhr.get(getArgs);
  460. getHandler.addCallback(function(data){
  461. try{
  462. self._getItemsFromLoadedData(data);
  463. self._loadFinished = true;
  464. self._loadInProgress = false;
  465. filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions));
  466. self._handleQueuedFetches();
  467. }catch(e){
  468. self._loadFinished = true;
  469. self._loadInProgress = false;
  470. errorCallback(e, keywordArgs);
  471. }
  472. });
  473. getHandler.addErrback(function(error){
  474. self._loadInProgress = false;
  475. errorCallback(error, keywordArgs);
  476. });
  477. //Wire up the cancel to abort of the request
  478. //This call cancel on the deferred if it hasn't been called
  479. //yet and then will chain to the simple abort of the
  480. //simpleFetch keywordArgs
  481. var oldAbort = null;
  482. if(keywordArgs.abort){
  483. oldAbort = keywordArgs.abort;
  484. }
  485. keywordArgs.abort = function(){
  486. var df = getHandler;
  487. if(df && df.fired === -1){
  488. df.cancel();
  489. df = null;
  490. }
  491. if(oldAbort){
  492. oldAbort.call(keywordArgs);
  493. }
  494. };
  495. }
  496. }else if(this._jsonData){
  497. try{
  498. this._loadFinished = true;
  499. this._getItemsFromLoadedData(this._jsonData);
  500. this._jsonData = null;
  501. filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
  502. }catch(e){
  503. errorCallback(e, keywordArgs);
  504. }
  505. }else{
  506. errorCallback(new Error("dojox.data.AndOrReadStore: No JSON source data was provided as either URL or a nested Javascript object."), keywordArgs);
  507. }
  508. } //end deferred fetching.
  509. }, //end _fetchItems
  510. _handleQueuedFetches: function(){
  511. // summary:
  512. // Internal function to execute delayed request in the store.
  513. //Execute any deferred fetches now.
  514. if(this._queuedFetches.length > 0){
  515. for(var i = 0; i < this._queuedFetches.length; i++){
  516. var fData = this._queuedFetches[i];
  517. var delayedQuery = fData.args;
  518. var delayedFilter = fData.filter;
  519. if(delayedFilter){
  520. delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions));
  521. }else{
  522. this.fetchItemByIdentity(delayedQuery);
  523. }
  524. }
  525. this._queuedFetches = [];
  526. }
  527. },
  528. _getItemsArray: function(/*object?*/queryOptions){
  529. // summary:
  530. // Internal function to determine which list of items to search over.
  531. // queryOptions: The query options parameter, if any.
  532. if(queryOptions && queryOptions.deep){
  533. return this._arrayOfAllItems;
  534. }
  535. return this._arrayOfTopLevelItems;
  536. },
  537. close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
  538. // summary:
  539. // See dojo.data.api.Read.close()
  540. if(this.clearOnClose &&
  541. this._loadFinished &&
  542. !this._loadInProgress){
  543. //Reset all internalsback to default state. This will force a reload
  544. //on next fetch. This also checks that the data or url param was set
  545. //so that the store knows it can get data. Without one of those being set,
  546. //the next fetch will trigger an error.
  547. if(((this._jsonFileUrl == "" || this._jsonFileUrl == null) &&
  548. (this.url == "" || this.url == null)
  549. ) && this.data == null){
  550. console.debug("dojox.data.AndOrReadStore: WARNING! Data reload " +
  551. " information has not been provided." +
  552. " Please set 'url' or 'data' to the appropriate value before" +
  553. " the next fetch");
  554. }
  555. this._arrayOfAllItems = [];
  556. this._arrayOfTopLevelItems = [];
  557. this._loadFinished = false;
  558. this._itemsByIdentity = null;
  559. this._loadInProgress = false;
  560. this._queuedFetches = [];
  561. }
  562. },
  563. _getItemsFromLoadedData: function(/* Object */ dataObject){
  564. // summary:
  565. // Function to parse the loaded data into item format and build the internal items array.
  566. // description:
  567. // Function to parse the loaded data into item format and build the internal items array.
  568. //
  569. // dataObject:
  570. // The JS data object containing the raw data to convery into item format.
  571. //
  572. // returns: array
  573. // Array of items in store item format.
  574. // First, we define a couple little utility functions...
  575. var self = this;
  576. function valueIsAnItem(/* anything */ aValue){
  577. // summary:
  578. // Given any sort of value that could be in the raw json data,
  579. // return true if we should interpret the value as being an
  580. // item itself, rather than a literal value or a reference.
  581. // example:
  582. // | false == valueIsAnItem("Kermit");
  583. // | false == valueIsAnItem(42);
  584. // | false == valueIsAnItem(new Date());
  585. // | false == valueIsAnItem({_type:'Date', _value:'May 14, 1802'});
  586. // | false == valueIsAnItem({_reference:'Kermit'});
  587. // | true == valueIsAnItem({name:'Kermit', color:'green'});
  588. // | true == valueIsAnItem({iggy:'pop'});
  589. // | true == valueIsAnItem({foo:42});
  590. var isItem = (
  591. (aValue !== null) &&
  592. (typeof aValue === "object") &&
  593. (!lang.isArray(aValue)) &&
  594. (!lang.isFunction(aValue)) &&
  595. (aValue.constructor == Object) &&
  596. (typeof aValue._reference === "undefined") &&
  597. (typeof aValue._type === "undefined") &&
  598. (typeof aValue._value === "undefined") &&
  599. self.hierarchical
  600. );
  601. return isItem;
  602. }
  603. function addItemAndSubItemsToArrayOfAllItems(/* Item */ anItem){
  604. self._arrayOfAllItems.push(anItem);
  605. for(var attribute in anItem){
  606. var valueForAttribute = anItem[attribute];
  607. if(valueForAttribute){
  608. if(lang.isArray(valueForAttribute)){
  609. var valueArray = valueForAttribute;
  610. for(var k = 0; k < valueArray.length; ++k){
  611. var singleValue = valueArray[k];
  612. if(valueIsAnItem(singleValue)){
  613. addItemAndSubItemsToArrayOfAllItems(singleValue);
  614. }
  615. }
  616. }else{
  617. if(valueIsAnItem(valueForAttribute)){
  618. addItemAndSubItemsToArrayOfAllItems(valueForAttribute);
  619. }
  620. }
  621. }
  622. }
  623. }
  624. this._labelAttr = dataObject.label;
  625. // We need to do some transformations to convert the data structure
  626. // that we read from the file into a format that will be convenient
  627. // to work with in memory.
  628. // Step 1: Walk through the object hierarchy and build a list of all items
  629. var i;
  630. var item;
  631. this._arrayOfAllItems = [];
  632. this._arrayOfTopLevelItems = dataObject.items;
  633. for(i = 0; i < this._arrayOfTopLevelItems.length; ++i){
  634. item = this._arrayOfTopLevelItems[i];
  635. addItemAndSubItemsToArrayOfAllItems(item);
  636. item[this._rootItemPropName]=true;
  637. }
  638. // Step 2: Walk through all the attribute values of all the items,
  639. // and replace single values with arrays. For example, we change this:
  640. // { name:'Miss Piggy', pets:'Foo-Foo'}
  641. // into this:
  642. // { name:['Miss Piggy'], pets:['Foo-Foo']}
  643. //
  644. // We also store the attribute names so we can validate our store
  645. // reference and item id special properties for the O(1) isItem
  646. var allAttributeNames = {};
  647. var key;
  648. for(i = 0; i < this._arrayOfAllItems.length; ++i){
  649. item = this._arrayOfAllItems[i];
  650. for(key in item){
  651. if(key !== this._rootItemPropName){
  652. var value = item[key];
  653. if(value !== null){
  654. if(!lang.isArray(value)){
  655. item[key] = [value];
  656. }
  657. }else{
  658. item[key] = [null];
  659. }
  660. }
  661. allAttributeNames[key]=key;
  662. }
  663. }
  664. // Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName
  665. // This should go really fast, it will generally never even run the loop.
  666. while(allAttributeNames[this._storeRefPropName]){
  667. this._storeRefPropName += "_";
  668. }
  669. while(allAttributeNames[this._itemNumPropName]){
  670. this._itemNumPropName += "_";
  671. }
  672. while(allAttributeNames[this._reverseRefMap]){
  673. this._reverseRefMap += "_";
  674. }
  675. // Step 4: Some data files specify an optional 'identifier', which is
  676. // the name of an attribute that holds the identity of each item.
  677. // If this data file specified an identifier attribute, then build a
  678. // hash table of items keyed by the identity of the items.
  679. var arrayOfValues;
  680. var identifier = dataObject.identifier;
  681. if(identifier){
  682. this._itemsByIdentity = {};
  683. this._features['dojo.data.api.Identity'] = identifier;
  684. for(i = 0; i < this._arrayOfAllItems.length; ++i){
  685. item = this._arrayOfAllItems[i];
  686. arrayOfValues = item[identifier];
  687. var identity = arrayOfValues[0];
  688. if(!this._itemsByIdentity[identity]){
  689. this._itemsByIdentity[identity] = item;
  690. }else{
  691. if(this._jsonFileUrl){
  692. throw new Error("dojox.data.AndOrReadStore: The json data as specified by: [" + this._jsonFileUrl + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]");
  693. }else if(this._jsonData){
  694. throw new Error("dojox.data.AndOrReadStore: The json data provided by the creation arguments is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]");
  695. }
  696. }
  697. }
  698. }else{
  699. this._features['dojo.data.api.Identity'] = Number;
  700. }
  701. // Step 5: Walk through all the items, and set each item's properties
  702. // for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true.
  703. for(i = 0; i < this._arrayOfAllItems.length; ++i){
  704. item = this._arrayOfAllItems[i];
  705. item[this._storeRefPropName] = this;
  706. item[this._itemNumPropName] = i;
  707. }
  708. // Step 6: We walk through all the attribute values of all the items,
  709. // looking for type/value literals and item-references.
  710. //
  711. // We replace item-references with pointers to items. For example, we change:
  712. // { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
  713. // into this:
  714. // { name:['Kermit'], friends:[miss_piggy] }
  715. // (where miss_piggy is the object representing the 'Miss Piggy' item).
  716. //
  717. // We replace type/value pairs with typed-literals. For example, we change:
  718. // { name:['Nelson Mandela'], born:[{_type:'Date', _value:'July 18, 1918'}] }
  719. // into this:
  720. // { name:['Kermit'], born:(new Date('July 18, 1918')) }
  721. //
  722. // We also generate the associate map for all items for the O(1) isItem function.
  723. for(i = 0; i < this._arrayOfAllItems.length; ++i){
  724. item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
  725. for(key in item){
  726. arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}]
  727. for(var j = 0; j < arrayOfValues.length; ++j){
  728. value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}}
  729. if(value !== null && typeof value == "object"){
  730. if(("_type" in value) && ("_value" in value)){
  731. var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber'
  732. var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}}
  733. if(!mappingObj){
  734. throw new Error("dojox.data.AndOrReadStore: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'");
  735. }else if(lang.isFunction(mappingObj)){
  736. arrayOfValues[j] = new mappingObj(value._value);
  737. }else if(lang.isFunction(mappingObj.deserialize)){
  738. arrayOfValues[j] = mappingObj.deserialize(value._value);
  739. }else{
  740. throw new Error("dojox.data.AndOrReadStore: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function");
  741. }
  742. }
  743. if(value._reference){
  744. var referenceDescription = value._reference; // example: {name:'Miss Piggy'}
  745. if(!lang.isObject(referenceDescription)){
  746. // example: 'Miss Piggy'
  747. // from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]}
  748. arrayOfValues[j] = this._getItemByIdentity(referenceDescription);
  749. }else{
  750. // example: {name:'Miss Piggy'}
  751. // from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
  752. for(var k = 0; k < this._arrayOfAllItems.length; ++k){
  753. var candidateItem = this._arrayOfAllItems[k];
  754. var found = true;
  755. for(var refKey in referenceDescription){
  756. if(candidateItem[refKey] != referenceDescription[refKey]){
  757. found = false;
  758. }
  759. }
  760. if(found){
  761. arrayOfValues[j] = candidateItem;
  762. }
  763. }
  764. }
  765. if(this.referenceIntegrity){
  766. var refItem = arrayOfValues[j];
  767. if(this.isItem(refItem)){
  768. this._addReferenceToMap(refItem, item, key);
  769. }
  770. }
  771. }else if(this.isItem(value)){
  772. //It's a child item (not one referenced through _reference).
  773. //We need to treat this as a referenced item, so it can be cleaned up
  774. //in a write store easily.
  775. if(this.referenceIntegrity){
  776. this._addReferenceToMap(value, item, key);
  777. }
  778. }
  779. }
  780. }
  781. }
  782. }
  783. },
  784. _addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){
  785. // summary:
  786. // Method to add an reference map entry for an item and attribute.
  787. // description:
  788. // Method to add an reference map entry for an item and attribute. //
  789. // refItem:
  790. // The item that is referenced.
  791. // parentItem:
  792. // The item that holds the new reference to refItem.
  793. // attribute:
  794. // The attribute on parentItem that contains the new reference.
  795. //Stub function, does nothing. Real processing is in ItemFileWriteStore.
  796. },
  797. getIdentity: function(/* item */ item){
  798. // summary:
  799. // See dojo.data.api.Identity.getIdentity()
  800. var identifier = this._features['dojo.data.api.Identity'];
  801. if(identifier === Number){
  802. return item[this._itemNumPropName]; // Number
  803. }else{
  804. var arrayOfValues = item[identifier];
  805. if(arrayOfValues){
  806. return arrayOfValues[0]; // Object || String
  807. }
  808. }
  809. return null; // null
  810. },
  811. fetchItemByIdentity: function(/* Object */ keywordArgs){
  812. // summary:
  813. // See dojo.data.api.Identity.fetchItemByIdentity()
  814. // Hasn't loaded yet, we have to trigger the load.
  815. if(!this._loadFinished){
  816. var self = this;
  817. if(this._jsonFileUrl !== this._ccUrl){
  818. kernel.deprecated("dojox.data.AndOrReadStore: ",
  819. "To change the url, set the url property of the store," +
  820. " not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0");
  821. this._ccUrl = this._jsonFileUrl;
  822. this.url = this._jsonFileUrl;
  823. }else if(this.url !== this._ccUrl){
  824. this._jsonFileUrl = this.url;
  825. this._ccUrl = this.url;
  826. }
  827. //See if there was any forced reset of data.
  828. if(this.data != null && this._jsonData == null){
  829. this._jsonData = this.data;
  830. this.data = null;
  831. }
  832. if(this._jsonFileUrl){
  833. if(this._loadInProgress){
  834. this._queuedFetches.push({args: keywordArgs});
  835. }else{
  836. this._loadInProgress = true;
  837. var getArgs = {
  838. url: self._jsonFileUrl,
  839. handleAs: "json-comment-optional",
  840. preventCache: this.urlPreventCache
  841. };
  842. var getHandler = xhr.get(getArgs);
  843. getHandler.addCallback(function(data){
  844. var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  845. try{
  846. self._getItemsFromLoadedData(data);
  847. self._loadFinished = true;
  848. self._loadInProgress = false;
  849. var item = self._getItemByIdentity(keywordArgs.identity);
  850. if(keywordArgs.onItem){
  851. keywordArgs.onItem.call(scope, item);
  852. }
  853. self._handleQueuedFetches();
  854. }catch(error){
  855. self._loadInProgress = false;
  856. if(keywordArgs.onError){
  857. keywordArgs.onError.call(scope, error);
  858. }
  859. }
  860. });
  861. getHandler.addErrback(function(error){
  862. self._loadInProgress = false;
  863. if(keywordArgs.onError){
  864. var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  865. keywordArgs.onError.call(scope, error);
  866. }
  867. });
  868. }
  869. }else if(this._jsonData){
  870. // Passed in data, no need to xhr.
  871. self._getItemsFromLoadedData(self._jsonData);
  872. self._jsonData = null;
  873. self._loadFinished = true;
  874. var item = self._getItemByIdentity(keywordArgs.identity);
  875. if(keywordArgs.onItem){
  876. var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  877. keywordArgs.onItem.call(scope, item);
  878. }
  879. }
  880. }else{
  881. // Already loaded. We can just look it up and call back.
  882. var item = this._getItemByIdentity(keywordArgs.identity);
  883. if(keywordArgs.onItem){
  884. var scope = keywordArgs.scope?keywordArgs.scope:winUtil.global;
  885. keywordArgs.onItem.call(scope, item);
  886. }
  887. }
  888. },
  889. _getItemByIdentity: function(/* Object */ identity){
  890. // summary:
  891. // Internal function to look an item up by its identity map.
  892. var item = null;
  893. if(this._itemsByIdentity){
  894. item = this._itemsByIdentity[identity];
  895. }else{
  896. item = this._arrayOfAllItems[identity];
  897. }
  898. if(item === undefined){
  899. item = null;
  900. }
  901. return item; // Object
  902. },
  903. getIdentityAttributes: function(/* item */ item){
  904. // summary:
  905. // See dojo.data.api.Identity.getIdentifierAttributes()
  906. var identifier = this._features['dojo.data.api.Identity'];
  907. if(identifier === Number){
  908. // If (identifier === Number) it means getIdentity() just returns
  909. // an integer item-number for each item. The dojo.data.api.Identity
  910. // spec says we need to return null if the identity is not composed
  911. // of attributes
  912. return null; // null
  913. }else{
  914. return [identifier]; // Array
  915. }
  916. },
  917. _forceLoad: function(){
  918. // summary:
  919. // Internal function to force a load of the store if it hasn't occurred yet. This is required
  920. // for specific functions to work properly.
  921. var self = this;
  922. if(this._jsonFileUrl !== this._ccUrl){
  923. kernel.deprecated("dojox.data.AndOrReadStore: ",
  924. "To change the url, set the url property of the store," +
  925. " not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0");
  926. this._ccUrl = this._jsonFileUrl;
  927. this.url = this._jsonFileUrl;
  928. }else if(this.url !== this._ccUrl){
  929. this._jsonFileUrl = this.url;
  930. this._ccUrl = this.url;
  931. }
  932. //See if there was any forced reset of data.
  933. if(this.data != null && this._jsonData == null){
  934. this._jsonData = this.data;
  935. this.data = null;
  936. }
  937. if(this._jsonFileUrl){
  938. var getArgs = {
  939. url: self._jsonFileUrl,
  940. handleAs: "json-comment-optional",
  941. preventCache: this.urlPreventCache,
  942. sync: true
  943. };
  944. var getHandler = xhr.get(getArgs);
  945. getHandler.addCallback(function(data){
  946. try{
  947. //Check to be sure there wasn't another load going on concurrently
  948. //So we don't clobber data that comes in on it. If there is a load going on
  949. //then do not save this data. It will potentially clobber current data.
  950. //We mainly wanted to sync/wait here.
  951. //TODO: Revisit the loading scheme of this store to improve multi-initial
  952. //request handling.
  953. if(self._loadInProgress !== true && !self._loadFinished){
  954. self._getItemsFromLoadedData(data);
  955. self._loadFinished = true;
  956. }else if(self._loadInProgress){
  957. //Okay, we hit an error state we can't recover from. A forced load occurred
  958. //while an async load was occurring. Since we cannot block at this point, the best
  959. //that can be managed is to throw an error.
  960. throw new Error("dojox.data.AndOrReadStore: Unable to perform a synchronous load, an async load is in progress.");
  961. }
  962. }catch(e){
  963. console.log(e);
  964. throw e;
  965. }
  966. });
  967. getHandler.addErrback(function(error){
  968. throw error;
  969. });
  970. }else if(this._jsonData){
  971. self._getItemsFromLoadedData(self._jsonData);
  972. self._jsonData = null;
  973. self._loadFinished = true;
  974. }
  975. }
  976. });
  977. //Mix in the simple fetch implementation to this class.
  978. lang.extend(AndOrReadStore, simpleFetch);
  979. return AndOrReadStore;
  980. });