AndOrReadStore.js 38 KB

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