QueryReadStore.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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.QueryReadStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.data.QueryReadStore"] = true;
  8. dojo.provide("dojox.data.QueryReadStore");
  9. dojo.require("dojo.data.util.sorter");
  10. dojo.require("dojo.string");
  11. dojo.declare("dojox.data.QueryReadStore",
  12. null,
  13. {
  14. // summary:
  15. // This class provides a store that is mainly intended to be used
  16. // for loading data dynamically from the server, used i.e. for
  17. // retreiving chunks of data from huge data stores on the server (by server-side filtering!).
  18. // Upon calling the fetch() method of this store the data are requested from
  19. // the server if they are not yet loaded for paging (or cached).
  20. //
  21. // For example used for a combobox which works on lots of data. It
  22. // can be used to retreive the data partially upon entering the
  23. // letters "ac" it returns only items like "action", "acting", etc.
  24. //
  25. // note:
  26. // The field name "id" in a query is reserved for looking up data
  27. // by id. This is necessary as before the first fetch, the store
  28. // has no way of knowing which field the server will declare as
  29. // identifier.
  30. //
  31. // example:
  32. // | // The parameter "query" contains the data that are sent to the server.
  33. // | var store = new dojox.data.QueryReadStore({url:'/search.php'});
  34. // | store.fetch({query:{name:'a'}, queryOptions:{ignoreCase:false}});
  35. //
  36. // | // Since "serverQuery" is given, it overrules and those data are
  37. // | // sent to the server.
  38. // | var store = new dojox.data.QueryReadStore({url:'/search.php'});
  39. // | store.fetch({serverQuery:{name:'a'}, queryOptions:{ignoreCase:false}});
  40. //
  41. // | <div dojoType="dojox.data.QueryReadStore"
  42. // | jsId="store2"
  43. // | url="../tests/stores/QueryReadStore.php"
  44. // | requestMethod="post"></div>
  45. // | <div dojoType="dojox.grid.data.DojoData"
  46. // | jsId="model2"
  47. // | store="store2"
  48. // | sortFields="[{attribute: 'name', descending: true}]"
  49. // | rowsPerPage="30"></div>
  50. // | <div dojoType="dojox.Grid" id="grid2"
  51. // | model="model2"
  52. // | structure="gridLayout"
  53. // | style="height:300px; width:800px;"></div>
  54. //
  55. // todo:
  56. // - there is a bug in the paging, when i set start:2, count:5 after an initial fetch() and doClientPaging:true
  57. // it returns 6 elemetns, though count=5, try it in QueryReadStore.html
  58. // - add optional caching
  59. // - when the first query searched for "a" and the next for a subset of
  60. // the first, i.e. "ab" then we actually dont need a server request, if
  61. // we have client paging, we just need to filter the items we already have
  62. // that might also be tooo much logic
  63. url:"",
  64. requestMethod:"get",
  65. //useCache:false,
  66. // We use the name in the errors, once the name is fixed hardcode it, may be.
  67. _className:"dojox.data.QueryReadStore",
  68. // This will contain the items we have loaded from the server.
  69. // The contents of this array is optimized to satisfy all read-api requirements
  70. // and for using lesser storage, so the keys and their content need some explaination:
  71. // this._items[0].i - the item itself
  72. // this._items[0].r - a reference to the store, so we can identify the item
  73. // securly. We set this reference right after receiving the item from the
  74. // server.
  75. _items:[],
  76. // Store the last query that triggered xhr request to the server.
  77. // So we can compare if the request changed and if we shall reload
  78. // (this also depends on other factors, such as is caching used, etc).
  79. _lastServerQuery:null,
  80. // Store how many rows we have so that we can pass it to a clientPaging handler
  81. _numRows:-1,
  82. // Store a hash of the last server request. Actually I introduced this
  83. // for testing, so I can check if no unnecessary requests were issued for
  84. // client-side-paging.
  85. lastRequestHash:null,
  86. // summary:
  87. // By default every request for paging is sent to the server.
  88. doClientPaging:false,
  89. // summary:
  90. // By default all the sorting is done serverside before the data is returned
  91. // which is the proper place to be doing it for really large datasets.
  92. doClientSorting:false,
  93. // Items by identify for Identify API
  94. _itemsByIdentity:null,
  95. // Identifier used
  96. _identifier:null,
  97. _features: {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true},
  98. _labelAttr: "label",
  99. constructor: function(/* Object */ params){
  100. dojo.mixin(this,params);
  101. },
  102. getValue: function(/* item */ item, /* attribute-name-string */ attribute, /* value? */ defaultValue){
  103. // According to the Read API comments in getValue() and exception is
  104. // thrown when an item is not an item or the attribute not a string!
  105. this._assertIsItem(item);
  106. if(!dojo.isString(attribute)){
  107. throw new Error(this._className+".getValue(): Invalid attribute, string expected!");
  108. }
  109. if(!this.hasAttribute(item, attribute)){
  110. // read api says: return defaultValue "only if *item* does not have a value for *attribute*."
  111. // Is this the case here? The attribute doesn't exist, but a defaultValue, sounds reasonable.
  112. if(defaultValue){
  113. return defaultValue;
  114. }
  115. }
  116. return item.i[attribute];
  117. },
  118. getValues: function(/* item */ item, /* attribute-name-string */ attribute){
  119. this._assertIsItem(item);
  120. var ret = [];
  121. if(this.hasAttribute(item, attribute)){
  122. ret.push(item.i[attribute]);
  123. }
  124. return ret;
  125. },
  126. getAttributes: function(/* item */ item){
  127. this._assertIsItem(item);
  128. var ret = [];
  129. for(var i in item.i){
  130. ret.push(i);
  131. }
  132. return ret;
  133. },
  134. hasAttribute: function(/* item */ item, /* attribute-name-string */ attribute){
  135. // summary:
  136. // See dojo.data.api.Read.hasAttribute()
  137. return this.isItem(item) && typeof item.i[attribute]!="undefined";
  138. },
  139. containsValue: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ value){
  140. var values = this.getValues(item, attribute);
  141. var len = values.length;
  142. for(var i=0; i<len; i++){
  143. if(values[i] == value){
  144. return true;
  145. }
  146. }
  147. return false;
  148. },
  149. isItem: function(/* anything */ something){
  150. // Some basic tests, that are quick and easy to do here.
  151. // >>> var store = new dojox.data.QueryReadStore({});
  152. // >>> store.isItem("");
  153. // false
  154. //
  155. // >>> var store = new dojox.data.QueryReadStore({});
  156. // >>> store.isItem({});
  157. // false
  158. //
  159. // >>> var store = new dojox.data.QueryReadStore({});
  160. // >>> store.isItem(0);
  161. // false
  162. //
  163. // >>> var store = new dojox.data.QueryReadStore({});
  164. // >>> store.isItem({name:"me", label:"me too"});
  165. // false
  166. //
  167. if(something){
  168. return typeof something.r != "undefined" && something.r == this;
  169. }
  170. return false;
  171. },
  172. isItemLoaded: function(/* anything */ something){
  173. // Currently we dont have any state that tells if an item is loaded or not
  174. // if the item exists its also loaded.
  175. // This might change when we start working with refs inside items ...
  176. return this.isItem(something);
  177. },
  178. loadItem: function(/* object */ args){
  179. if(this.isItemLoaded(args.item)){
  180. return;
  181. }
  182. // Actually we have nothing to do here, or at least I dont know what to do here ...
  183. },
  184. fetch:function(/* Object? */ request){
  185. // summary:
  186. // See dojo.data.util.simpleFetch.fetch() this is just a copy and I adjusted
  187. // only the paging, since it happens on the server if doClientPaging is
  188. // false, thx to http://trac.dojotoolkit.org/ticket/4761 reporting this.
  189. // Would be nice to be able to use simpleFetch() to reduce copied code,
  190. // but i dont know how yet. Ideas please!
  191. request = request || {};
  192. if(!request.store){
  193. request.store = this;
  194. }
  195. var self = this;
  196. var _errorHandler = function(errorData, requestObject){
  197. if(requestObject.onError){
  198. var scope = requestObject.scope || dojo.global;
  199. requestObject.onError.call(scope, errorData, requestObject);
  200. }
  201. };
  202. var _fetchHandler = function(items, requestObject, numRows){
  203. var oldAbortFunction = requestObject.abort || null;
  204. var aborted = false;
  205. var startIndex = requestObject.start?requestObject.start:0;
  206. if(self.doClientPaging == false){
  207. // For client paging we dont need no slicing of the result.
  208. startIndex = 0;
  209. }
  210. var endIndex = requestObject.count?(startIndex + requestObject.count):items.length;
  211. requestObject.abort = function(){
  212. aborted = true;
  213. if(oldAbortFunction){
  214. oldAbortFunction.call(requestObject);
  215. }
  216. };
  217. var scope = requestObject.scope || dojo.global;
  218. if(!requestObject.store){
  219. requestObject.store = self;
  220. }
  221. if(requestObject.onBegin){
  222. requestObject.onBegin.call(scope, numRows, requestObject);
  223. }
  224. if(requestObject.sort && self.doClientSorting){
  225. items.sort(dojo.data.util.sorter.createSortFunction(requestObject.sort, self));
  226. }
  227. if(requestObject.onItem){
  228. for(var i = startIndex; (i < items.length) && (i < endIndex); ++i){
  229. var item = items[i];
  230. if(!aborted){
  231. requestObject.onItem.call(scope, item, requestObject);
  232. }
  233. }
  234. }
  235. if(requestObject.onComplete && !aborted){
  236. var subset = null;
  237. if(!requestObject.onItem){
  238. subset = items.slice(startIndex, endIndex);
  239. }
  240. requestObject.onComplete.call(scope, subset, requestObject);
  241. }
  242. };
  243. this._fetchItems(request, _fetchHandler, _errorHandler);
  244. return request; // Object
  245. },
  246. getFeatures: function(){
  247. return this._features;
  248. },
  249. close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
  250. // I have no idea if this is really needed ...
  251. },
  252. getLabel: function(/* item */ item){
  253. // summary:
  254. // See dojo.data.api.Read.getLabel()
  255. if(this._labelAttr && this.isItem(item)){
  256. return this.getValue(item, this._labelAttr); //String
  257. }
  258. return undefined; //undefined
  259. },
  260. getLabelAttributes: function(/* item */ item){
  261. // summary:
  262. // See dojo.data.api.Read.getLabelAttributes()
  263. if(this._labelAttr){
  264. return [this._labelAttr]; //array
  265. }
  266. return null; //null
  267. },
  268. _xhrFetchHandler: function(data, request, fetchHandler, errorHandler){
  269. data = this._filterResponse(data);
  270. if(data.label){
  271. this._labelAttr = data.label;
  272. }
  273. var numRows = data.numRows || -1;
  274. this._items = [];
  275. // Store a ref to "this" in each item, so we can simply check if an item
  276. // really origins form here (idea is from ItemFileReadStore, I just don't know
  277. // how efficient the real storage use, garbage collection effort, etc. is).
  278. dojo.forEach(data.items,function(e){
  279. this._items.push({i:e, r:this});
  280. },this);
  281. var identifier = data.identifier;
  282. this._itemsByIdentity = {};
  283. if(identifier){
  284. this._identifier = identifier;
  285. var i;
  286. for(i = 0; i < this._items.length; ++i){
  287. var item = this._items[i].i;
  288. var identity = item[identifier];
  289. if(!this._itemsByIdentity[identity]){
  290. this._itemsByIdentity[identity] = item;
  291. }else{
  292. throw new Error(this._className+": The json data as specified by: [" + this.url + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]");
  293. }
  294. }
  295. }else{
  296. this._identifier = Number;
  297. for(i = 0; i < this._items.length; ++i){
  298. this._items[i].n = i;
  299. }
  300. }
  301. // TODO actually we should do the same as dojo.data.ItemFileReadStore._getItemsFromLoadedData() to sanitize
  302. // (does it really sanititze them) and store the data optimal. should we? for security reasons???
  303. numRows = this._numRows = (numRows === -1) ? this._items.length : numRows;
  304. fetchHandler(this._items, request, numRows);
  305. this._numRows = numRows;
  306. },
  307. _fetchItems: function(request, fetchHandler, errorHandler){
  308. // summary:
  309. // The request contains the data as defined in the Read-API.
  310. // Additionally there is following keyword "serverQuery".
  311. //
  312. // The *serverQuery* parameter, optional.
  313. // This parameter contains the data that will be sent to the server.
  314. // If this parameter is not given the parameter "query"'s
  315. // data are sent to the server. This is done for some reasons:
  316. // - to specify explicitly which data are sent to the server, they
  317. // might also be a mix of what is contained in "query", "queryOptions"
  318. // and the paging parameters "start" and "count" or may be even
  319. // completely different things.
  320. // - don't modify the request.query data, so the interface using this
  321. // store can rely on unmodified data, as the combobox dijit currently
  322. // does it, it compares if the query has changed
  323. // - request.query is required by the Read-API
  324. //
  325. // I.e. the following examples might be sent via GET:
  326. // fetch({query:{name:"abc"}, queryOptions:{ignoreCase:true}})
  327. // the URL will become: /url.php?name=abc
  328. //
  329. // fetch({serverQuery:{q:"abc", c:true}, query:{name:"abc"}, queryOptions:{ignoreCase:true}})
  330. // the URL will become: /url.php?q=abc&c=true
  331. // // The serverQuery-parameter has overruled the query-parameter
  332. // // but the query parameter stays untouched, but is not sent to the server!
  333. // // The serverQuery contains more data than the query, so they might differ!
  334. //
  335. var serverQuery = request.serverQuery || request.query || {};
  336. //Need to add start and count
  337. if(!this.doClientPaging){
  338. serverQuery.start = request.start || 0;
  339. // Count might not be sent if not given.
  340. if(request.count){
  341. serverQuery.count = request.count;
  342. }
  343. }
  344. if(!this.doClientSorting && request.sort){
  345. var sortInfo = [];
  346. dojo.forEach(request.sort, function(sort){
  347. if(sort && sort.attribute){
  348. sortInfo.push((sort.descending ? "-" : "") + sort.attribute);
  349. }
  350. });
  351. serverQuery.sort = sortInfo.join(',');
  352. }
  353. // Compare the last query and the current query by simply json-encoding them,
  354. // so we dont have to do any deep object compare ... is there some dojo.areObjectsEqual()???
  355. if(this.doClientPaging && this._lastServerQuery !== null &&
  356. dojo.toJson(serverQuery) == dojo.toJson(this._lastServerQuery)
  357. ){
  358. this._numRows = (this._numRows === -1) ? this._items.length : this._numRows;
  359. fetchHandler(this._items, request, this._numRows);
  360. }else{
  361. var xhrFunc = this.requestMethod.toLowerCase() == "post" ? dojo.xhrPost : dojo.xhrGet;
  362. var xhrHandler = xhrFunc({url:this.url, handleAs:"json-comment-optional", content:serverQuery, failOk: true});
  363. request.abort = function(){
  364. xhrHandler.cancel();
  365. };
  366. xhrHandler.addCallback(dojo.hitch(this, function(data){
  367. this._xhrFetchHandler(data, request, fetchHandler, errorHandler);
  368. }));
  369. xhrHandler.addErrback(function(error){
  370. errorHandler(error, request);
  371. });
  372. // Generate the hash using the time in milliseconds and a randon number.
  373. // Since Math.randon() returns something like: 0.23453463, we just remove the "0."
  374. // probably just for esthetic reasons :-).
  375. this.lastRequestHash = new Date().getTime()+"-"+String(Math.random()).substring(2);
  376. this._lastServerQuery = dojo.mixin({}, serverQuery);
  377. }
  378. },
  379. _filterResponse: function(data){
  380. // summary:
  381. // If the data from servers needs to be processed before it can be processed by this
  382. // store, then this function should be re-implemented in subclass. This default
  383. // implementation just return the data unchanged.
  384. // data:
  385. // The data received from server
  386. return data;
  387. },
  388. _assertIsItem: function(/* item */ item){
  389. // summary:
  390. // It throws an error if item is not valid, so you can call it in every method that needs to
  391. // throw an error when item is invalid.
  392. // item:
  393. // The item to test for being contained by the store.
  394. if(!this.isItem(item)){
  395. throw new Error(this._className+": Invalid item argument.");
  396. }
  397. },
  398. _assertIsAttribute: function(/* attribute-name-string */ attribute){
  399. // summary:
  400. // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
  401. // attribute:
  402. // The attribute to test for being contained by the store.
  403. if(typeof attribute !== "string"){
  404. throw new Error(this._className+": Invalid attribute argument ('"+attribute+"').");
  405. }
  406. },
  407. fetchItemByIdentity: function(/* Object */ keywordArgs){
  408. // summary:
  409. // See dojo.data.api.Identity.fetchItemByIdentity()
  410. // See if we have already loaded the item with that id
  411. // In case there hasn't been a fetch yet, _itemsByIdentity is null
  412. // and thus a fetch will be triggered below.
  413. if(this._itemsByIdentity){
  414. var item = this._itemsByIdentity[keywordArgs.identity];
  415. if(!(item === undefined)){
  416. if(keywordArgs.onItem){
  417. var scope = keywordArgs.scope ? keywordArgs.scope : dojo.global;
  418. keywordArgs.onItem.call(scope, {i:item, r:this});
  419. }
  420. return;
  421. }
  422. }
  423. // Otherwise we need to go remote
  424. // Set up error handler
  425. var _errorHandler = function(errorData, requestObject){
  426. var scope = keywordArgs.scope ? keywordArgs.scope : dojo.global;
  427. if(keywordArgs.onError){
  428. keywordArgs.onError.call(scope, errorData);
  429. }
  430. };
  431. // Set up fetch handler
  432. var _fetchHandler = function(items, requestObject){
  433. var scope = keywordArgs.scope ? keywordArgs.scope : dojo.global;
  434. try{
  435. // There is supposed to be only one result
  436. var item = null;
  437. if(items && items.length == 1){
  438. item = items[0];
  439. }
  440. // If no item was found, item is still null and we'll
  441. // fire the onItem event with the null here
  442. if(keywordArgs.onItem){
  443. keywordArgs.onItem.call(scope, item);
  444. }
  445. }catch(error){
  446. if(keywordArgs.onError){
  447. keywordArgs.onError.call(scope, error);
  448. }
  449. }
  450. };
  451. // Construct query
  452. var request = {serverQuery:{id:keywordArgs.identity}};
  453. // Dispatch query
  454. this._fetchItems(request, _fetchHandler, _errorHandler);
  455. },
  456. getIdentity: function(/* item */ item){
  457. // summary:
  458. // See dojo.data.api.Identity.getIdentity()
  459. var identifier = null;
  460. if(this._identifier === Number){
  461. identifier = item.n; // Number
  462. }else{
  463. identifier = item.i[this._identifier];
  464. }
  465. return identifier;
  466. },
  467. getIdentityAttributes: function(/* item */ item){
  468. // summary:
  469. // See dojo.data.api.Identity.getIdentityAttributes()
  470. return [this._identifier];
  471. }
  472. }
  473. );
  474. }