OpmlStore.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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.OpmlStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.data.OpmlStore"] = true;
  8. dojo.provide("dojox.data.OpmlStore");
  9. dojo.require("dojo.data.util.simpleFetch");
  10. dojo.require("dojo.data.util.filter");
  11. dojo.declare("dojox.data.OpmlStore", null, {
  12. /* summary:
  13. * The OpmlStore implements the dojo.data.api.Read API.
  14. */
  15. /* examples:
  16. * var opmlStore = new dojo.data.OpmlStore({url:"geography.xml"});
  17. * var opmlStore = new dojo.data.OpmlStore({url:"http://example.com/geography.xml"});
  18. */
  19. constructor: function(/* Object */ keywordParameters){
  20. // summary: constructor
  21. // keywordParameters: {url: String, label: String} Where label is optional and configures what should be used as the return from getLabel()
  22. this._xmlData = null;
  23. this._arrayOfTopLevelItems = [];
  24. this._arrayOfAllItems = [];
  25. this._metadataNodes = null;
  26. this._loadFinished = false;
  27. this.url = keywordParameters.url;
  28. this._opmlData = keywordParameters.data; // XML DOM Document
  29. if(keywordParameters.label){
  30. this.label = keywordParameters.label;
  31. }
  32. this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset.
  33. this._queuedFetches = [];
  34. this._identityMap = {};
  35. this._identCount = 0;
  36. this._idProp = "_I";
  37. if(keywordParameters && "urlPreventCache" in keywordParameters){
  38. this.urlPreventCache = keywordParameters.urlPreventCache?true:false;
  39. }
  40. },
  41. // label: [public] string
  42. // The attribute of the Opml item to act as a label.
  43. label: "text",
  44. // url: [public] string
  45. // The location from which to fetch the Opml document.
  46. url: "",
  47. // urlPreventCache: [public] boolean
  48. // Flag to denote if the underlying xhrGet call should set preventCache.
  49. urlPreventCache: false,
  50. _assertIsItem: function(/* item */ item){
  51. if(!this.isItem(item)){
  52. throw new Error("dojo.data.OpmlStore: a function was passed an item argument that was not an item");
  53. }
  54. },
  55. _assertIsAttribute: function(/* item || String */ attribute){
  56. // summary:
  57. // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
  58. // attribute:
  59. // The attribute to test for being contained by the store.
  60. if(!dojo.isString(attribute)){
  61. throw new Error("dojox.data.OpmlStore: a function was passed an attribute argument that was not an attribute object nor an attribute name string");
  62. }
  63. },
  64. _removeChildNodesThatAreNotElementNodes: function(/* node */ node, /* boolean */ recursive){
  65. var childNodes = node.childNodes;
  66. if(childNodes.length === 0){
  67. return;
  68. }
  69. var nodesToRemove = [];
  70. var i, childNode;
  71. for(i = 0; i < childNodes.length; ++i){
  72. childNode = childNodes[i];
  73. if(childNode.nodeType != 1){
  74. nodesToRemove.push(childNode);
  75. }
  76. }
  77. for(i = 0; i < nodesToRemove.length; ++i){
  78. childNode = nodesToRemove[i];
  79. node.removeChild(childNode);
  80. }
  81. if(recursive){
  82. for(i = 0; i < childNodes.length; ++i){
  83. childNode = childNodes[i];
  84. this._removeChildNodesThatAreNotElementNodes(childNode, recursive);
  85. }
  86. }
  87. },
  88. _processRawXmlTree: function(/* xmlDoc */ rawXmlTree){
  89. this._loadFinished = true;
  90. this._xmlData = rawXmlTree;
  91. var headNodes = rawXmlTree.getElementsByTagName('head');
  92. var headNode = headNodes[0];
  93. if(headNode){
  94. this._removeChildNodesThatAreNotElementNodes(headNode);
  95. this._metadataNodes = headNode.childNodes;
  96. }
  97. var bodyNodes = rawXmlTree.getElementsByTagName('body');
  98. var bodyNode = bodyNodes[0];
  99. if(bodyNode){
  100. this._removeChildNodesThatAreNotElementNodes(bodyNode, true);
  101. var bodyChildNodes = bodyNodes[0].childNodes;
  102. for(var i = 0; i < bodyChildNodes.length; ++i){
  103. var node = bodyChildNodes[i];
  104. if(node.tagName == 'outline'){
  105. this._identityMap[this._identCount] = node;
  106. this._identCount++;
  107. this._arrayOfTopLevelItems.push(node);
  108. this._arrayOfAllItems.push(node);
  109. this._checkChildNodes(node);
  110. }
  111. }
  112. }
  113. },
  114. _checkChildNodes: function(node /*Node*/){
  115. // summary:
  116. // Internal function to recurse over all child nodes from the store and add them
  117. // As non-toplevel items
  118. // description:
  119. // Internal function to recurse over all child nodes from the store and add them
  120. // As non-toplevel items
  121. //
  122. // node:
  123. // The child node to walk.
  124. if(node.firstChild){
  125. for(var i = 0; i < node.childNodes.length; i++){
  126. var child = node.childNodes[i];
  127. if(child.tagName == 'outline'){
  128. this._identityMap[this._identCount] = child;
  129. this._identCount++;
  130. this._arrayOfAllItems.push(child);
  131. this._checkChildNodes(child);
  132. }
  133. }
  134. }
  135. },
  136. _getItemsArray: function(/*object?*/queryOptions){
  137. // summary:
  138. // Internal function to determine which list of items to search over.
  139. // queryOptions: The query options parameter, if any.
  140. if(queryOptions && queryOptions.deep){
  141. return this._arrayOfAllItems;
  142. }
  143. return this._arrayOfTopLevelItems;
  144. },
  145. /***************************************
  146. dojo.data.api.Read API
  147. ***************************************/
  148. getValue: function( /* item */ item,
  149. /* attribute || attribute-name-string */ attribute,
  150. /* value? */ defaultValue){
  151. // summary:
  152. // See dojo.data.api.Read.getValue()
  153. this._assertIsItem(item);
  154. this._assertIsAttribute(attribute);
  155. if(attribute == 'children'){
  156. return (item.firstChild || defaultValue); //Object
  157. }else{
  158. var value = item.getAttribute(attribute);
  159. return (value !== undefined) ? value : defaultValue; //Object
  160. }
  161. },
  162. getValues: function(/* item */ item,
  163. /* attribute || attribute-name-string */ attribute){
  164. // summary:
  165. // See dojo.data.api.Read.getValues()
  166. this._assertIsItem(item);
  167. this._assertIsAttribute(attribute);
  168. var array = [];
  169. if(attribute == 'children'){
  170. for(var i = 0; i < item.childNodes.length; ++i){
  171. array.push(item.childNodes[i]);
  172. }
  173. } else if(item.getAttribute(attribute) !== null){
  174. array.push(item.getAttribute(attribute));
  175. }
  176. return array; // Array
  177. },
  178. getAttributes: function(/* item */ item){
  179. // summary:
  180. // See dojo.data.api.Read.getAttributes()
  181. this._assertIsItem(item);
  182. var attributes = [];
  183. var xmlNode = item;
  184. var xmlAttributes = xmlNode.attributes;
  185. for(var i = 0; i < xmlAttributes.length; ++i){
  186. var xmlAttribute = xmlAttributes.item(i);
  187. attributes.push(xmlAttribute.nodeName);
  188. }
  189. if(xmlNode.childNodes.length > 0){
  190. attributes.push('children');
  191. }
  192. return attributes; //Array
  193. },
  194. hasAttribute: function( /* item */ item,
  195. /* attribute || attribute-name-string */ attribute){
  196. // summary:
  197. // See dojo.data.api.Read.hasAttribute()
  198. return (this.getValues(item, attribute).length > 0); //Boolean
  199. },
  200. containsValue: function(/* item */ item,
  201. /* attribute || attribute-name-string */ attribute,
  202. /* anything */ value){
  203. // summary:
  204. // See dojo.data.api.Read.containsValue()
  205. var regexp = undefined;
  206. if(typeof value === "string"){
  207. regexp = dojo.data.util.filter.patternToRegExp(value, false);
  208. }
  209. return this._containsValue(item, attribute, value, regexp); //boolean.
  210. },
  211. _containsValue: function( /* item */ item,
  212. /* attribute || attribute-name-string */ attribute,
  213. /* anything */ value,
  214. /* RegExp?*/ regexp){
  215. // summary:
  216. // Internal function for looking at the values contained by the item.
  217. // description:
  218. // Internal function for looking at the values contained by the item. This
  219. // function allows for denoting if the comparison should be case sensitive for
  220. // strings or not (for handling filtering cases where string case should not matter)
  221. //
  222. // item:
  223. // The data item to examine for attribute values.
  224. // attribute:
  225. // The attribute to inspect.
  226. // value:
  227. // The value to match.
  228. // regexp:
  229. // Optional regular expression generated off value if value was of string type to handle wildcarding.
  230. // If present and attribute values are string, then it can be used for comparison instead of 'value'
  231. var values = this.getValues(item, attribute);
  232. for(var i = 0; i < values.length; ++i){
  233. var possibleValue = values[i];
  234. if(typeof possibleValue === "string" && regexp){
  235. return (possibleValue.match(regexp) !== null);
  236. }else{
  237. //Non-string matching.
  238. if(value === possibleValue){
  239. return true; // Boolean
  240. }
  241. }
  242. }
  243. return false; // Boolean
  244. },
  245. isItem: function(/* anything */ something){
  246. // summary:
  247. // See dojo.data.api.Read.isItem()
  248. // description:
  249. // Four things are verified to ensure that "something" is an item:
  250. // something can not be null, the nodeType must be an XML Element,
  251. // the tagName must be "outline", and the node must be a member of
  252. // XML document for this datastore.
  253. return (something &&
  254. something.nodeType == 1 &&
  255. something.tagName == 'outline' &&
  256. something.ownerDocument === this._xmlData); //Boolean
  257. },
  258. isItemLoaded: function(/* anything */ something){
  259. // summary:
  260. // See dojo.data.api.Read.isItemLoaded()
  261. // OpmlStore loads every item, so if it's an item, then it's loaded.
  262. return this.isItem(something); //Boolean
  263. },
  264. loadItem: function(/* item */ item){
  265. // summary:
  266. // See dojo.data.api.Read.loadItem()
  267. // description:
  268. // The OpmlStore always loads all items, so if it's an item, then it's loaded.
  269. // From the dojo.data.api.Read.loadItem docs:
  270. // If a call to isItemLoaded() returns true before loadItem() is even called,
  271. // then loadItem() need not do any work at all and will not even invoke the callback handlers.
  272. },
  273. getLabel: function(/* item */ item){
  274. // summary:
  275. // See dojo.data.api.Read.getLabel()
  276. if(this.isItem(item)){
  277. return this.getValue(item,this.label); //String
  278. }
  279. return undefined; //undefined
  280. },
  281. getLabelAttributes: function(/* item */ item){
  282. // summary:
  283. // See dojo.data.api.Read.getLabelAttributes()
  284. return [this.label]; //array
  285. },
  286. // The dojo.data.api.Read.fetch() function is implemented as
  287. // a mixin from dojo.data.util.simpleFetch.
  288. // That mixin requires us to define _fetchItems().
  289. _fetchItems: function( /* Object */ keywordArgs,
  290. /* Function */ findCallback,
  291. /* Function */ errorCallback){
  292. // summary:
  293. // See dojo.data.util.simpleFetch.fetch()
  294. var self = this;
  295. var filter = function(requestArgs, arrayOfItems){
  296. var items = null;
  297. if(requestArgs.query){
  298. items = [];
  299. var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false;
  300. //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the
  301. //same value for each item examined. Much more efficient.
  302. var regexpList = {};
  303. for(var key in requestArgs.query){
  304. var value = requestArgs.query[key];
  305. if(typeof value === "string"){
  306. regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
  307. }
  308. }
  309. for(var i = 0; i < arrayOfItems.length; ++i){
  310. var match = true;
  311. var candidateItem = arrayOfItems[i];
  312. for(var key in requestArgs.query){
  313. var value = requestArgs.query[key];
  314. if(!self._containsValue(candidateItem, key, value, regexpList[key])){
  315. match = false;
  316. }
  317. }
  318. if(match){
  319. items.push(candidateItem);
  320. }
  321. }
  322. }else{
  323. // We want a copy to pass back in case the parent wishes to sort the array. We shouldn't allow resort
  324. // of the internal list so that multiple callers can get lists and sort without affecting each other.
  325. if(arrayOfItems.length> 0){
  326. items = arrayOfItems.slice(0,arrayOfItems.length);
  327. }
  328. }
  329. findCallback(items, requestArgs);
  330. };
  331. if(this._loadFinished){
  332. filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
  333. }else{
  334. //If fetches come in before the loading has finished, but while
  335. //a load is in progress, we have to defer the fetching to be
  336. //invoked in the callback.
  337. if(this._loadInProgress){
  338. this._queuedFetches.push({args: keywordArgs, filter: filter});
  339. }else{
  340. if(this.url !== ""){
  341. this._loadInProgress = true;
  342. var getArgs = {
  343. url: self.url,
  344. handleAs: "xml",
  345. preventCache: self.urlPreventCache
  346. };
  347. var getHandler = dojo.xhrGet(getArgs);
  348. getHandler.addCallback(function(data){
  349. self._processRawXmlTree(data);
  350. filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions));
  351. self._handleQueuedFetches();
  352. });
  353. getHandler.addErrback(function(error){
  354. throw error;
  355. });
  356. }else if(this._opmlData){
  357. this._processRawXmlTree(this._opmlData);
  358. this._opmlData = null;
  359. filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
  360. }else{
  361. throw new Error("dojox.data.OpmlStore: No OPML source data was provided as either URL or XML data input.");
  362. }
  363. }
  364. }
  365. },
  366. getFeatures: function(){
  367. // summary: See dojo.data.api.Read.getFeatures()
  368. var features = {
  369. 'dojo.data.api.Read': true,
  370. 'dojo.data.api.Identity': true
  371. };
  372. return features; //Object
  373. },
  374. /***************************************
  375. dojo.data.api.Identity API
  376. ***************************************/
  377. getIdentity: function(/* item */ item){
  378. // summary:
  379. // See dojo.data.api.Identity.getIdentity()
  380. if(this.isItem(item)){
  381. //No ther way to do this other than O(n) without
  382. //complete rework of how the tree stores nodes.
  383. for(var i in this._identityMap){
  384. if(this._identityMap[i] === item){
  385. return i;
  386. }
  387. }
  388. }
  389. return null; //null
  390. },
  391. fetchItemByIdentity: function(/* Object */ keywordArgs){
  392. // summary:
  393. // See dojo.data.api.Identity.fetchItemByIdentity()
  394. //Hasn't loaded yet, we have to trigger the load.
  395. if(!this._loadFinished){
  396. var self = this;
  397. if(this.url !== ""){
  398. //If fetches come in before the loading has finished, but while
  399. //a load is in progress, we have to defer the fetching to be
  400. //invoked in the callback.
  401. if(this._loadInProgress){
  402. this._queuedFetches.push({args: keywordArgs});
  403. }else{
  404. this._loadInProgress = true;
  405. var getArgs = {
  406. url: self.url,
  407. handleAs: "xml"
  408. };
  409. var getHandler = dojo.xhrGet(getArgs);
  410. getHandler.addCallback(function(data){
  411. var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
  412. try{
  413. self._processRawXmlTree(data);
  414. var item = self._identityMap[keywordArgs.identity];
  415. if(!self.isItem(item)){
  416. item = null;
  417. }
  418. if(keywordArgs.onItem){
  419. keywordArgs.onItem.call(scope, item);
  420. }
  421. self._handleQueuedFetches();
  422. }catch(error){
  423. if(keywordArgs.onError){
  424. keywordArgs.onError.call(scope, error);
  425. }
  426. }
  427. });
  428. getHandler.addErrback(function(error){
  429. this._loadInProgress = false;
  430. if(keywordArgs.onError){
  431. var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
  432. keywordArgs.onError.call(scope, error);
  433. }
  434. });
  435. }
  436. }else if(this._opmlData){
  437. this._processRawXmlTree(this._opmlData);
  438. this._opmlData = null;
  439. var item = this._identityMap[keywordArgs.identity];
  440. if(!self.isItem(item)){
  441. item = null;
  442. }
  443. if(keywordArgs.onItem){
  444. var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
  445. keywordArgs.onItem.call(scope, item);
  446. }
  447. }
  448. }else{
  449. //Already loaded. We can just look it up and call back.
  450. var item = this._identityMap[keywordArgs.identity];
  451. if(!this.isItem(item)){
  452. item = null;
  453. }
  454. if(keywordArgs.onItem){
  455. var scope = keywordArgs.scope?keywordArgs.scope:dojo.global;
  456. keywordArgs.onItem.call(scope, item);
  457. }
  458. }
  459. },
  460. getIdentityAttributes: function(/* item */ item){
  461. // summary:
  462. // See dojo.data.api.Identity.getIdentifierAttributes()
  463. //Identity isn't a public attribute in the item, it's the node count.
  464. //So, return null.
  465. return null;
  466. },
  467. _handleQueuedFetches: function(){
  468. // summary:
  469. // Internal function to execute delayed request in the store.
  470. //Execute any deferred fetches now.
  471. if(this._queuedFetches.length > 0){
  472. for(var i = 0; i < this._queuedFetches.length; i++){
  473. var fData = this._queuedFetches[i];
  474. var delayedQuery = fData.args;
  475. var delayedFilter = fData.filter;
  476. if(delayedFilter){
  477. delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions));
  478. }else{
  479. this.fetchItemByIdentity(delayedQuery);
  480. }
  481. }
  482. this._queuedFetches = [];
  483. }
  484. },
  485. close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
  486. // summary:
  487. // See dojo.data.api.Read.close()
  488. }
  489. });
  490. //Mix in the simple fetch implementation to this class.
  491. dojo.extend(dojox.data.OpmlStore,dojo.data.util.simpleFetch);
  492. }