dojo.provide("dojox.data.OpmlStore"); dojo.require("dojo.data.util.filter"); dojo.require("dojo.data.util.simpleFetch"); dojo.declare("dojox.data.OpmlStore", null, { /* summary: * The OpmlStore implements the dojo.data.api.Read API. */ /* examples: * var opmlStore = new dojo.data.OpmlStore({url:"geography.xml"}); * var opmlStore = new dojo.data.OpmlStore({url:"http://example.com/geography.xml"}); */ constructor: function(/* Object */ keywordParameters){ // summary: constructor // keywordParameters: {url: String, label: String} Where label is optional and configures what should be used as the return from getLabel() this._xmlData = null; this._arrayOfTopLevelItems = []; this._arrayOfAllItems = []; this._metadataNodes = null; this._loadFinished = false; this.url = keywordParameters.url; this._opmlData = keywordParameters.data; // XML DOM Document if(keywordParameters.label){ this.label = keywordParameters.label; } this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset. this._queuedFetches = []; this._identityMap = {}; this._identCount = 0; this._idProp = "_I"; }, label: "text", url: "", _assertIsItem: function(/* item */ item){ if(!this.isItem(item)){ throw new Error("dojo.data.OpmlStore: a function was passed an item argument that was not an item"); } }, _assertIsAttribute: function(/* item || String */ attribute){ // summary: // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store. // attribute: // The attribute to test for being contained by the store. if(!dojo.isString(attribute)){ throw new Error("dojox.data.OpmlStore: a function was passed an attribute argument that was not an attribute object nor an attribute name string"); } }, _removeChildNodesThatAreNotElementNodes: function(/* node */ node, /* boolean */ recursive){ var childNodes = node.childNodes; if(childNodes.length === 0){ return; } var nodesToRemove = []; var i, childNode; for(i = 0; i < childNodes.length; ++i){ childNode = childNodes[i]; if(childNode.nodeType != 1){ nodesToRemove.push(childNode); } } for(i = 0; i < nodesToRemove.length; ++i){ childNode = nodesToRemove[i]; node.removeChild(childNode); } if(recursive){ for(i = 0; i < childNodes.length; ++i){ childNode = childNodes[i]; this._removeChildNodesThatAreNotElementNodes(childNode, recursive); } } }, _processRawXmlTree: function(/* xmlDoc */ rawXmlTree){ this._loadFinished = true; this._xmlData = rawXmlTree; var headNodes = rawXmlTree.getElementsByTagName('head'); var headNode = headNodes[0]; if(headNode){ this._removeChildNodesThatAreNotElementNodes(headNode); this._metadataNodes = headNode.childNodes; } var bodyNodes = rawXmlTree.getElementsByTagName('body'); var bodyNode = bodyNodes[0]; if(bodyNode){ this._removeChildNodesThatAreNotElementNodes(bodyNode, true); var bodyChildNodes = bodyNodes[0].childNodes; for(var i = 0; i < bodyChildNodes.length; ++i){ var node = bodyChildNodes[i]; if(node.tagName == 'outline'){ this._identityMap[this._identCount] = node; this._identCount++; this._arrayOfTopLevelItems.push(node); this._arrayOfAllItems.push(node); this._checkChildNodes(node); } } } }, _checkChildNodes: function(node /*Node*/){ // summary: // Internal function to recurse over all child nodes from the store and add them // As non-toplevel items // description: // Internal function to recurse over all child nodes from the store and add them // As non-toplevel items // // node: // The child node to walk. if(node.firstChild){ for(var i = 0; i < node.childNodes.length; i++){ var child = node.childNodes[i]; if(child.tagName == 'outline'){ this._identityMap[this._identCount] = child; this._identCount++; this._arrayOfAllItems.push(child); this._checkChildNodes(child); } } } }, _getItemsArray: function(/*object?*/queryOptions){ // summary: // Internal function to determine which list of items to search over. // queryOptions: The query options parameter, if any. if(queryOptions && queryOptions.deep) { return this._arrayOfAllItems; } return this._arrayOfTopLevelItems; }, /*************************************** dojo.data.api.Read API ***************************************/ getValue: function( /* item */ item, /* attribute || attribute-name-string */ attribute, /* value? */ defaultValue){ // summary: // See dojo.data.api.Read.getValue() this._assertIsItem(item); this._assertIsAttribute(attribute); if(attribute == 'children'){ return (item.firstChild || defaultValue); //Object } else { var value = item.getAttribute(attribute); return (value !== undefined) ? value : defaultValue; //Object } }, getValues: function(/* item */ item, /* attribute || attribute-name-string */ attribute){ // summary: // See dojo.data.api.Read.getValues() this._assertIsItem(item); this._assertIsAttribute(attribute); var array = []; if(attribute == 'children'){ for(var i = 0; i < item.childNodes.length; ++i){ array.push(item.childNodes[i]); } } else if(item.getAttribute(attribute) !== null){ array.push(item.getAttribute(attribute)); } return array; // Array }, getAttributes: function(/* item */ item){ // summary: // See dojo.data.api.Read.getAttributes() this._assertIsItem(item); var attributes = []; var xmlNode = item; var xmlAttributes = xmlNode.attributes; for(var i = 0; i < xmlAttributes.length; ++i){ var xmlAttribute = xmlAttributes.item(i); attributes.push(xmlAttribute.nodeName); } if(xmlNode.childNodes.length > 0){ attributes.push('children'); } return attributes; //Array }, hasAttribute: function( /* item */ item, /* attribute || attribute-name-string */ attribute){ // summary: // See dojo.data.api.Read.hasAttribute() return (this.getValues(item, attribute).length > 0); //Boolean }, containsValue: function(/* item */ item, /* attribute || attribute-name-string */ attribute, /* anything */ value){ // summary: // See dojo.data.api.Read.containsValue() var regexp = undefined; if(typeof value === "string"){ regexp = dojo.data.util.filter.patternToRegExp(value, false); } return this._containsValue(item, attribute, value, regexp); //boolean. }, _containsValue: function( /* item */ item, /* attribute || attribute-name-string */ attribute, /* anything */ value, /* RegExp?*/ regexp){ // summary: // Internal function for looking at the values contained by the item. // description: // Internal function for looking at the values contained by the item. This // function allows for denoting if the comparison should be case sensitive for // strings or not (for handling filtering cases where string case should not matter) // // item: // The data item to examine for attribute values. // attribute: // The attribute to inspect. // value: // The value to match. // regexp: // Optional regular expression generated off value if value was of string type to handle wildcarding. // If present and attribute values are string, then it can be used for comparison instead of 'value' var values = this.getValues(item, attribute); for(var i = 0; i < values.length; ++i){ var possibleValue = values[i]; if(typeof possibleValue === "string" && regexp){ return (possibleValue.match(regexp) !== null); }else{ //Non-string matching. if(value === possibleValue){ return true; // Boolean } } } return false; // Boolean }, isItem: function(/* anything */ something){ // summary: // See dojo.data.api.Read.isItem() // description: // Four things are verified to ensure that "something" is an item: // something can not be null, the nodeType must be an XML Element, // the tagName must be "outline", and the node must be a member of // XML document for this datastore. return (something && something.nodeType == 1 && something.tagName == 'outline' && something.ownerDocument === this._xmlData); //Boolean }, isItemLoaded: function(/* anything */ something){ // summary: // See dojo.data.api.Read.isItemLoaded() // OpmlStore loads every item, so if it's an item, then it's loaded. return this.isItem(something); //Boolean }, loadItem: function(/* item */ item){ // summary: // See dojo.data.api.Read.loadItem() // description: // The OpmlStore always loads all items, so if it's an item, then it's loaded. // From the dojo.data.api.Read.loadItem docs: // If a call to isItemLoaded() returns true before loadItem() is even called, // then loadItem() need not do any work at all and will not even invoke the callback handlers. }, getLabel: function(/* item */ item){ // summary: // See dojo.data.api.Read.getLabel() if(this.isItem(item)){ return this.getValue(item,this.label); //String } return undefined; //undefined }, getLabelAttributes: function(/* item */ item){ // summary: // See dojo.data.api.Read.getLabelAttributes() return [this.label]; //array }, // The dojo.data.api.Read.fetch() function is implemented as // a mixin from dojo.data.util.simpleFetch. // That mixin requires us to define _fetchItems(). _fetchItems: function( /* Object */ keywordArgs, /* Function */ findCallback, /* Function */ errorCallback){ // summary: // See dojo.data.util.simpleFetch.fetch() var self = this; var filter = function(requestArgs, arrayOfItems){ var items = null; if(requestArgs.query){ items = []; var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false; //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the //same value for each item examined. Much more efficient. var regexpList = {}; for(var key in requestArgs.query){ var value = requestArgs.query[key]; if(typeof value === "string"){ regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase); } } for(var i = 0; i < arrayOfItems.length; ++i){ var match = true; var candidateItem = arrayOfItems[i]; for(var key in requestArgs.query){ var value = requestArgs.query[key]; if(!self._containsValue(candidateItem, key, value, regexpList[key])){ match = false; } } if(match){ items.push(candidateItem); } } }else{ // We want a copy to pass back in case the parent wishes to sort the array. We shouldn't allow resort // of the internal list so that multiple callers can get lists and sort without affecting each other. if(arrayOfItems.length> 0){ items = arrayOfItems.slice(0,arrayOfItems.length); } } findCallback(items, requestArgs); }; if(this._loadFinished){ filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions)); }else{ //If fetches come in before the loading has finished, but while //a load is in progress, we have to defer the fetching to be //invoked in the callback. if(this._loadInProgress){ this._queuedFetches.push({args: keywordArgs, filter: filter}); }else{ if(this.url !== ""){ this._loadInProgress = true; var getArgs = { url: self.url, handleAs: "xml" }; var getHandler = dojo.xhrGet(getArgs); getHandler.addCallback(function(data){ self._processRawXmlTree(data); filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions)); self._handleQueuedFetches(); }); getHandler.addErrback(function(error){ throw error; }); }else if(this._opmlData){ this._processRawXmlTree(this._opmlData); this._opmlData = null; filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions)); }else{ throw new Error("dojox.data.OpmlStore: No OPML source data was provided as either URL or XML data input."); } } } }, getFeatures: function(){ // summary: See dojo.data.api.Read.getFeatures() var features = { 'dojo.data.api.Read': true, 'dojo.data.api.Identity': true }; return features; //Object }, /*************************************** dojo.data.api.Identity API ***************************************/ getIdentity: function(/* item */ item){ // summary: // See dojo.data.api.Identity.getIdentity() if(this.isItem(item)){ //No ther way to do this other than O(n) without //complete rework of how the tree stores nodes. for(var i in this._identityMap){ if(this._identityMap[i] === item){ return i; } } } return null; //null }, fetchItemByIdentity: function(/* Object */ keywordArgs){ // summary: // See dojo.data.api.Identity.fetchItemByIdentity() //Hasn't loaded yet, we have to trigger the load. if(!this._loadFinished){ var self = this; if(this.url !== ""){ //If fetches come in before the loading has finished, but while //a load is in progress, we have to defer the fetching to be //invoked in the callback. if(this._loadInProgress){ this._queuedFetches.push({args: keywordArgs}); }else{ this._loadInProgress = true; var getArgs = { url: self.url, handleAs: "xml" }; var getHandler = dojo.xhrGet(getArgs); getHandler.addCallback(function(data){ var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; try{ self._processRawXmlTree(data); var item = self._identityMap[keywordArgs.identity]; if(!self.isItem(item)){ item = null; } if(keywordArgs.onItem){ keywordArgs.onItem.call(scope, item); } self._handleQueuedFetches(); }catch(error){ if(keywordArgs.onError){ keywordArgs.onError.call(scope, error); } } }); getHandler.addErrback(function(error){ this._loadInProgress = false; if(keywordArgs.onError){ var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; keywordArgs.onError.call(scope, error); } }); } }else if(this._opmlData){ this._processRawXmlTree(this._opmlData); this._opmlData = null; var item = this._identityMap[keywordArgs.identity]; if(!self.isItem(item)){ item = null; } if(keywordArgs.onItem){ var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; keywordArgs.onItem.call(scope, item); } } }else{ //Already loaded. We can just look it up and call back. var item = this._identityMap[keywordArgs.identity]; if(!this.isItem(item)){ item = null; } if(keywordArgs.onItem){ var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; keywordArgs.onItem.call(scope, item); } } }, getIdentityAttributes: function(/* item */ item){ // summary: // See dojo.data.api.Identity.getIdentifierAttributes() //Identity isn't a public attribute in the item, it's the node count. //So, return null. return null; }, _handleQueuedFetches: function(){ // summary: // Internal function to execute delayed request in the store. //Execute any deferred fetches now. if (this._queuedFetches.length > 0) { for(var i = 0; i < this._queuedFetches.length; i++){ var fData = this._queuedFetches[i]; var delayedQuery = fData.args; var delayedFilter = fData.filter; if(delayedFilter){ delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions)); }else{ this.fetchItemByIdentity(delayedQuery); } } this._queuedFetches = []; } }, close: function(/*dojo.data.api.Request || keywordArgs || null */ request){ // summary: // See dojo.data.api.Read.close() } }); //Mix in the simple fetch implementation to this class. dojo.extend(dojox.data.OpmlStore,dojo.data.util.simpleFetch);