QueryReadStore.js 18 KB

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