diff options
Diffstat (limited to 'js/dojo/dojox/data/XmlStore.js')
| -rw-r--r-- | js/dojo/dojox/data/XmlStore.js | 1472 |
1 files changed, 1472 insertions, 0 deletions
diff --git a/js/dojo/dojox/data/XmlStore.js b/js/dojo/dojox/data/XmlStore.js new file mode 100644 index 0000000..d9dc91a --- /dev/null +++ b/js/dojo/dojox/data/XmlStore.js @@ -0,0 +1,1472 @@ +//>>built +define("dojox/data/XmlStore", ["dojo/_base/lang", "dojo/_base/declare", "dojo/_base/xhr", "dojo/data/util/simpleFetch", + "dojo/_base/query", "dojo/_base/array", "dojo/_base/window", "dojo/data/util/filter", "dojox/xml/parser", + "dojox/data/XmlItem"], + function(lang, declare, xhr, simpleFetch, domQuery, array, winUtil, filter, xmlParser, XmlItem) { + +var XmlStore = declare("dojox.data.XmlStore", null, { + // summary: + // A data store for XML based services or documents + // description: + // A data store for XML based services or documents + + constructor: function(/* object */ args){ + // summary: + // Constructor for the XML store. + // args: + // An anonymous object to initialize properties. It expects the following values: + // url: The url to a service or an XML document that represents the store + // rootItem: A tag name for root items + // keyAttribute: An attribute name for a key or an identity (unique identifier) + // Required for serverside fetchByIdentity, etc. Not required for + // client side fetchItemBIdentity, as it will use an XPath-like + // structure if keyAttribute was not specified. Recommended to always + // set this, though, for consistent identity behavior. + // attributeMap: An anonymous object contains properties for attribute mapping, + // {"tag_name.item_attribute_name": "@xml_attribute_name", ...} + // sendQuery: A boolean indicate to add a query string to the service URL. + // Default is false. + // urlPreventCache: Parameter to indicate whether or not URL calls should apply + // the preventCache option to the xhr request. + if(args){ + this.url = args.url; + this.rootItem = (args.rootItem || args.rootitem || this.rootItem); + this.keyAttribute = (args.keyAttribute || args.keyattribute || this.keyAttribute); + this._attributeMap = (args.attributeMap || args.attributemap); + this.label = args.label || this.label; + this.sendQuery = (args.sendQuery || args.sendquery || this.sendQuery); + if("urlPreventCache" in args){ + this.urlPreventCache = args.urlPreventCache?true:false; + } + } + this._newItems = []; + this._deletedItems = []; + this._modifiedItems = []; + }, + + //Values that may be set by the parser. + //Ergo, have to be instantiated to something + //So the parser knows how to set them. + url: "", + + // A tag name for XML tags to be considered root items in the hierarchy + rootItem: "", + + // An attribute name for a key or an identity (unique identifier) + // Required for serverside fetchByIdentity, etc. Not required for + // client side fetchItemBIdentity, as it will use an XPath-like + // structure if keyAttribute was not specified. Recommended to always + // set this, though, for consistent identity behavior. + keyAttribute: "", + + // An attribute of the item to use as the label. + label: "", + + // A boolean indicate to add a query string to the service URL. + // Default is false. + sendQuery: false, + + // An anonymous object that contains properties for attribute mapping, + // for example {"tag_name.item_attribute_name": "@xml_attribute_name", ...}. + // This is optional. This is done so that attributes which are actual + // XML tag attributes (and not sub-tags of an XML tag), can be referenced. + attributeMap: null, + + // Parameter to indicate whether or not URL calls should apply the preventCache option to the xhr request. + urlPreventCache: true, + + /* dojo.data.api.Read */ + + getValue: function(/* item */ item, /* attribute || attribute-name-string */ attribute, /* value? */ defaultValue){ + // summary: + // Return an attribute value + // description: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // If 'attribute' specifies "tagName", the tag name of the element is + // returned. + // If 'attribute' specifies "childNodes", the first element child is + // returned. + // If 'attribute' specifies "text()", the value of the first text + // child is returned. + // For generic attributes, if '_attributeMap' is specified, + // an actual attribute name is looked up with the tag name of + // the element and 'attribute' (concatenated with '.'). + // Then, if 'attribute' starts with "@", the value of the XML + // attribute is returned. + // Otherwise, the first child element of the tag name specified with + // 'attribute' is returned. + // item: + // An XML element that holds the attribute + // attribute: + // A tag name of a child element, An XML attribute name or one of + // special names + // defaultValue: + // A default value + // returns: + // An attribute value found, otherwise 'defaultValue' + var element = item.element; + var i; + var node; + if(attribute === "tagName"){ + return element.nodeName; + }else if(attribute === "childNodes"){ + for(i = 0; i < element.childNodes.length; i++){ + node = element.childNodes[i]; + if(node.nodeType === 1 /*ELEMENT_NODE*/){ + return this._getItem(node); //object + } + } + return defaultValue; + }else if(attribute === "text()"){ + for(i = 0; i < element.childNodes.length; i++){ + node = element.childNodes[i]; + if(node.nodeType === 3 /*TEXT_NODE*/ || + node.nodeType === 4 /*CDATA_SECTION_NODE*/){ + return node.nodeValue; //string + } + } + return defaultValue; + }else{ + attribute = this._getAttribute(element.nodeName, attribute); + if(attribute.charAt(0) === '@'){ + var name = attribute.substring(1); + var value = element.getAttribute(name); + //Note that getAttribute will return null or empty string for undefined/unset + //attributes, therefore, we should just check the return was valid + //non-empty string and not null. + return (value) ? value : defaultValue; //object + }else{ + for(i = 0; i < element.childNodes.length; i++){ + node = element.childNodes[i]; + if( node.nodeType === 1 /*ELEMENT_NODE*/ && + node.nodeName === attribute){ + return this._getItem(node); //object + } + } + return defaultValue; //object + } + } + }, + + getValues: function(/* item */ item, /* attribute || attribute-name-string */ attribute){ + // summary: + // Return an array of attribute values + // description: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // If 'attribute' specifies "tagName", the tag name of the element is + // returned. + // If 'attribute' specifies "childNodes", child elements are returned. + // If 'attribute' specifies "text()", the values of child text nodes + // are returned. + // For generic attributes, if 'attributeMap' is specified, + // an actual attribute name is looked up with the tag name of + // the element and 'attribute' (concatenated with '.'). + // Then, if 'attribute' starts with "@", the value of the XML + // attribute is returned. + // Otherwise, child elements of the tag name specified with + // 'attribute' are returned. + // item: + // An XML element that holds the attribute + // attribute: + // A tag name of child elements, An XML attribute name or one of + // special names + // returns: + // An array of attribute values found, otherwise an empty array + var element = item.element; + var values = []; + var i; + var node; + if(attribute === "tagName"){ + return [element.nodeName]; + }else if(attribute === "childNodes"){ + for(i = 0; i < element.childNodes.length; i++){ + node = element.childNodes[i]; + if(node.nodeType === 1 /*ELEMENT_NODE*/){ + values.push(this._getItem(node)); + } + } + return values; //array + }else if(attribute === "text()"){ + var ec = element.childNodes; + for(i = 0; i < ec.length; i++){ + node = ec[i]; + if(node.nodeType === 3 || node.nodeType === 4){ + values.push(node.nodeValue); + } + } + return values; //array + }else{ + attribute = this._getAttribute(element.nodeName, attribute); + if(attribute.charAt(0) === '@'){ + var name = attribute.substring(1); + var value = element.getAttribute(name); + return (value !== undefined) ? [value] : []; //array + }else{ + for(i = 0; i < element.childNodes.length; i++){ + node = element.childNodes[i]; + if( node.nodeType === 1 /*ELEMENT_NODE*/ && + node.nodeName === attribute){ + values.push(this._getItem(node)); + } + } + return values; //array + } + } + }, + + getAttributes: function(/* item */ item){ + // summary: + // Return an array of attribute names + // description: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // tag names of child elements and XML attribute names of attributes + // specified to the element are returned along with special attribute + // names applicable to the element including "tagName", "childNodes" + // if the element has child elements, "text()" if the element has + // child text nodes, and attribute names in '_attributeMap' that match + // the tag name of the element. + // item: + // An XML element + // returns: + // An array of attributes found + var element = item.element; + var attributes = []; + var i; + attributes.push("tagName"); + if(element.childNodes.length > 0){ + var names = {}; + var childNodes = true; + var text = false; + for(i = 0; i < element.childNodes.length; i++){ + var node = element.childNodes[i]; + if(node.nodeType === 1 /*ELEMENT_NODE*/){ + var name = node.nodeName; + if(!names[name]){ + attributes.push(name); + names[name] = name; + } + childNodes = true; + }else if(node.nodeType === 3){ + text = true; + } + } + if(childNodes){ + attributes.push("childNodes"); + } + if(text){ + attributes.push("text()"); + } + } + for(i = 0; i < element.attributes.length; i++){ + attributes.push("@" + element.attributes[i].nodeName); + } + if(this._attributeMap){ + for(var key in this._attributeMap){ + i = key.indexOf('.'); + if(i > 0){ + var tagName = key.substring(0, i); + if(tagName === element.nodeName){ + attributes.push(key.substring(i + 1)); + } + }else{ // global attribute + attributes.push(key); + } + } + } + return attributes; //array + }, + + hasAttribute: function(/* item */ item, /* attribute || attribute-name-string */ attribute){ + // summary: + // Check whether an element has the attribute + // item: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // attribute: + // A tag name of a child element, An XML attribute name or one of + // special names + // returns: + // True if the element has the attribute, otherwise false + return (this.getValue(item, attribute) !== undefined); //boolean + }, + + containsValue: function(/* item */ item, /* attribute || attribute-name-string */ attribute, /* anything */ value){ + // summary: + // Check whether the attribute values contain the value + // item: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // attribute: + // A tag name of a child element, An XML attribute name or one of + // special names + // returns: + // True if the attribute values contain the value, otherwise false + var values = this.getValues(item, attribute); + for(var i = 0; i < values.length; i++){ + if((typeof value === "string")){ + if(values[i].toString && values[i].toString() === value){ + return true; + } + }else if(values[i] === value){ + return true; //boolean + } + } + return false;//boolean + }, + + isItem: function(/* anything */ something){ + // summary: + // Check whether the object is an item (XML element) + // item: + // An object to check + // returns: + // True if the object is an XML element, otherwise false + if(something && something.element && something.store && something.store === this){ + return true; //boolean + } + return false; //boolran + }, + + isItemLoaded: function(/* anything */ something){ + // summary: + // Check whether the object is an item (XML element) and loaded + // item: + // An object to check + // returns: + // True if the object is an XML element, otherwise false + return this.isItem(something); //boolean + }, + + loadItem: function(/* object */ keywordArgs){ + // summary: + // Load an item (XML element) + // keywordArgs: + // object containing the args for loadItem. See dojo.data.api.Read.loadItem() + }, + + getFeatures: function(){ + // summary: + // Return supported data APIs + // returns: + // "dojo.data.api.Read" and "dojo.data.api.Write" + var features = { + "dojo.data.api.Read": true, + "dojo.data.api.Write": true + }; + + //Local XML parsing can implement Identity fairly simple via + if(!this.sendQuery || this.keyAttribute !== ""){ + features["dojo.data.api.Identity"] = true; + } + return features; //array + }, + + getLabel: function(/* item */ item){ + // summary: + // See dojo.data.api.Read.getLabel() + if((this.label !== "") && this.isItem(item)){ + var label = this.getValue(item,this.label); + if(label){ + return label.toString(); + } + } + return undefined; //undefined + }, + + getLabelAttributes: function(/* item */ item){ + // summary: + // See dojo.data.api.Read.getLabelAttributes() + if(this.label !== ""){ + return [this.label]; //array + } + return null; //null + }, + + _fetchItems: function(request, fetchHandler, errorHandler){ + // summary: + // Fetch items (XML elements) that match to a query + // description: + // If 'sendQuery' is true, an XML document is loaded from + // 'url' with a query string. + // Otherwise, an XML document is loaded and list XML elements that + // match to a query (set of element names and their text attribute + // values that the items to contain). + // A wildcard, "*" can be used to query values to match all + // occurrences. + // If 'rootItem' is specified, it is used to fetch items. + // request: + // A request object + // fetchHandler: + // A function to call for fetched items + // errorHandler: + // A function to call on error + var url = this._getFetchUrl(request); + if(!url){ + errorHandler(new Error("No URL specified."), request); + return; + } + var localRequest = (!this.sendQuery ? request : {}); // use request for _getItems() + + var self = this; + var getArgs = { + url: url, + handleAs: "xml", + preventCache: self.urlPreventCache + }; + var getHandler = xhr.get(getArgs); + getHandler.addCallback(function(data){ + var items = self._getItems(data, localRequest); + if(items && items.length > 0){ + fetchHandler(items, request); + }else{ + fetchHandler([], request); + } + }); + getHandler.addErrback(function(data){ + errorHandler(data, request); + }); + }, + + _getFetchUrl: function(request){ + // summary: + // Generate a URL for fetch + // description: + // This default implementation generates a query string in the form of + // "?name1=value1&name2=value2..." off properties of 'query' object + // specified in 'request' and appends it to 'url', if 'sendQuery' + // is set to false. + // Otherwise, 'url' is returned as is. + // Sub-classes may override this method for the custom URL generation. + // request: + // A request object + // returns: + // A fetch URL + if(!this.sendQuery){ + return this.url; + } + var query = request.query; + if(!query){ + return this.url; + } + if(lang.isString(query)){ + return this.url + query; + } + var queryString = ""; + for(var name in query){ + var value = query[name]; + if(value){ + if(queryString){ + queryString += "&"; + } + queryString += (name + "=" + value); + } + } + if(!queryString){ + return this.url; + } + //Check to see if the URL already has query params or not. + var fullUrl = this.url; + if(fullUrl.indexOf("?") < 0){ + fullUrl += "?"; + }else{ + fullUrl += "&"; + } + return fullUrl + queryString; + }, + + _getItems: function(document, request){ + // summary: + // Fetch items (XML elements) in an XML document based on a request + // description: + // This default implementation walks through child elements of + // the document element to see if all properties of 'query' object + // match corresponding attributes of the element (item). + // If 'request' is not specified, all child elements are returned. + // Sub-classes may override this method for the custom search in + // an XML document. + // document: + // An XML document + // request: + // A request object + // returns: + // An array of items + var query = null; + if(request){ + query = request.query; + } + var items = []; + var nodes = null; + + if(this.rootItem !== ""){ + nodes = domQuery(this.rootItem, document); + }else{ + nodes = document.documentElement.childNodes; + } + + var deep = request.queryOptions ? request.queryOptions.deep : false; + if(deep){ + nodes = this._flattenNodes(nodes); + } + for(var i = 0; i < nodes.length; i++){ + var node = nodes[i]; + if(node.nodeType != 1 /*ELEMENT_NODE*/){ + continue; + } + var item = this._getItem(node); + if(query){ + var ignoreCase = request.queryOptions ? request.queryOptions.ignoreCase : false; + var value; + var match = false; + var j; + var emptyQuery = true; + + //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 query){ + value = query[key]; + if(typeof value === "string"){ + regexpList[key] = filter.patternToRegExp(value, ignoreCase); + }else if(value){ + // It's an object, possibly regexp, so treat it as one. + regexpList[key] = value; + } + } + for(var attribute in query){ + emptyQuery = false; + var values = this.getValues(item, attribute); + for(j = 0; j < values.length; j++){ + value = values[j]; + if(value){ + var queryValue = query[attribute]; + if((typeof value) === "string" && + (regexpList[attribute])){ + if((value.match(regexpList[attribute])) !== null){ + match = true; + }else{ + match = false; + } + }else if((typeof value) === "object"){ + if( value.toString && + (regexpList[attribute])){ + var stringValue = value.toString(); + if((stringValue.match(regexpList[attribute])) !== null){ + match = true; + }else{ + match = false; + } + }else{ + if(queryValue === "*" || queryValue === value){ + match = true; + }else{ + match = false; + } + } + } + } + //One of the multiValue values matched, + //so quit looking. + if(match){ + break; + } + } + if(!match){ + break; + } + } + //Either the query was an empty object {}, which is match all, or + //was an actual match. + if(emptyQuery || match){ + items.push(item); + } + }else{ + //No query, everything matches. + items.push(item); + } + } + array.forEach(items,function(item){ + if(item.element.parentNode){ + item.element.parentNode.removeChild(item.element); // make it root + } + },this); + return items; + }, + + _flattenNodes: function(nodes){ + // Summary: + // Function used to flatten a hierarchy of XML nodes into a single list for + // querying over. Used when deep = true; + var flattened = []; + if(nodes){ + var i; + for(i = 0; i < nodes.length; i++){ + var node = nodes[i]; + flattened.push(node); + if(node.childNodes && node.childNodes.length > 0){ + flattened = flattened.concat(this._flattenNodes(node.childNodes)); + } + } + } + return flattened; + }, + + close: function(/*dojo.data.api.Request || keywordArgs || null */ request){ + // summary: + // See dojo.data.api.Read.close() + }, + +/* dojo.data.api.Write */ + + newItem: function(/* object? */ keywordArgs, parentInfo){ + // summary: + // Return a new dojox.data.XmlItem + // description: + // At least, 'keywordArgs' must contain "tagName" to be used for + // the new element. + // Other attributes in 'keywordArgs' are set to the new element, + // including "text()", but excluding "childNodes". + // keywordArgs: + // An object containing initial attributes + // returns: + // An XML element + keywordArgs = (keywordArgs || {}); + var tagName = keywordArgs.tagName; + if(!tagName){ + tagName = this.rootItem; + if(tagName === ""){ + return null; + } + } + + var document = this._getDocument(); + var element = document.createElement(tagName); + for(var attribute in keywordArgs){ + var text; + if(attribute === "tagName"){ + continue; + }else if(attribute === "text()"){ + text = document.createTextNode(keywordArgs[attribute]); + element.appendChild(text); + }else{ + attribute = this._getAttribute(tagName, attribute); + if(attribute.charAt(0) === '@'){ + var name = attribute.substring(1); + element.setAttribute(name, keywordArgs[attribute]); + }else{ + var child = document.createElement(attribute); + text = document.createTextNode(keywordArgs[attribute]); + child.appendChild(text); + element.appendChild(child); + } + } + } + + var item = this._getItem(element); + this._newItems.push(item); + + var pInfo = null; + if(parentInfo && parentInfo.parent && parentInfo.attribute){ + pInfo = { + item: parentInfo.parent, + attribute: parentInfo.attribute, + oldValue: undefined + }; + + //See if it is multi-valued or not and handle appropriately + //Generally, all attributes are multi-valued for this store + //So, we only need to append if there are already values present. + var values = this.getValues(parentInfo.parent, parentInfo.attribute); + if(values && values.length > 0){ + var tempValues = values.slice(0, values.length); + if(values.length === 1){ + pInfo.oldValue = values[0]; + }else{ + pInfo.oldValue = values.slice(0, values.length); + } + tempValues.push(item); + this.setValues(parentInfo.parent, parentInfo.attribute, tempValues); + pInfo.newValue = this.getValues(parentInfo.parent, parentInfo.attribute); + }else{ + this.setValue(parentInfo.parent, parentInfo.attribute, item); + pInfo.newValue = item; + } + } + return item; //object + }, + + deleteItem: function(/* item */ item){ + // summary: + // Delete an dojox.data.XmlItem (wrapper to a XML element). + // item: + // An XML element to delete + // returns: + // True + var element = item.element; + if(element.parentNode){ + this._backupItem(item); + element.parentNode.removeChild(element); + return true; + } + this._forgetItem(item); + this._deletedItems.push(item); + return true; //boolean + }, + + setValue: function(/* item */ item, /* attribute || string */ attribute, /* almost anything */ value){ + // summary: + // Set an attribute value + // description: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // If 'attribute' specifies "tagName", nothing is set and false is + // returned. + // If 'attribute' specifies "childNodes", the value (XML element) is + // added to the element. + // If 'attribute' specifies "text()", a text node is created with + // the value and set it to the element as a child. + // For generic attributes, if '_attributeMap' is specified, + // an actual attribute name is looked up with the tag name of + // the element and 'attribute' (concatenated with '.'). + // Then, if 'attribute' starts with "@", the value is set to the XML + // attribute. + // Otherwise, a text node is created with the value and set it to + // the first child element of the tag name specified with 'attribute'. + // If the child element does not exist, it is created. + // item: + // An XML element that holds the attribute + // attribute: + // A tag name of a child element, An XML attribute name or one of + // special names + // value: + // A attribute value to set + // returns: + // False for "tagName", otherwise true + if(attribute === "tagName"){ + return false; //boolean + } + + this._backupItem(item); + + var element = item.element; + var child; + var text; + if(attribute === "childNodes"){ + child = value.element; + element.appendChild(child); + }else if(attribute === "text()"){ + while(element.firstChild){ + element.removeChild(element.firstChild); + } + text = this._getDocument(element).createTextNode(value); + element.appendChild(text); + }else{ + attribute = this._getAttribute(element.nodeName, attribute); + if(attribute.charAt(0) === '@'){ + var name = attribute.substring(1); + element.setAttribute(name, value); + }else{ + for(var i = 0; i < element.childNodes.length; i++){ + var node = element.childNodes[i]; + if( node.nodeType === 1 /*ELEMENT_NODE*/ && + node.nodeName === attribute){ + child = node; + break; + } + } + var document = this._getDocument(element); + if(child){ + while(child.firstChild){ + child.removeChild(child.firstChild); + } + }else{ + child = document.createElement(attribute); + element.appendChild(child); + } + text = document.createTextNode(value); + child.appendChild(text); + } + } + return true; //boolean + }, + + setValues: function(/* item */ item, /* attribute || string */ attribute, /*array*/ values){ + // summary: + // Set attribute values + // description: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // If 'attribute' specifies "tagName", nothing is set and false is + // returned. + // If 'attribute' specifies "childNodes", the value (array of XML + // elements) is set to the element's childNodes. + // If 'attribute' specifies "text()", a text node is created with + // the values and set it to the element as a child. + // For generic attributes, if '_attributeMap' is specified, + // an actual attribute name is looked up with the tag name of + // the element and 'attribute' (concatenated with '.'). + // Then, if 'attribute' starts with "@", the first value is set to + // the XML attribute. + // Otherwise, child elements of the tag name specified with + // 'attribute' are replaced with new child elements and their + // child text nodes of values. + // item: + // An XML element that holds the attribute + // attribute: + // A tag name of child elements, an XML attribute name or one of + // special names + // value: + // A attribute value to set + // notify: + // A non-API optional argument, used to indicate if notification API should be called + // or not. + + // returns: + // False for "tagName", otherwise true + if(attribute === "tagName"){ + return false; //boolean + } + + this._backupItem(item); + + var element = item.element; + var i; + var child; + var text; + if(attribute === "childNodes"){ + while(element.firstChild){ + element.removeChild(element.firstChild); + } + for(i = 0; i < values.length; i++){ + child = values[i].element; + element.appendChild(child); + } + }else if(attribute === "text()"){ + while(element.firstChild){ + element.removeChild(element.firstChild); + } + var value = ""; + for(i = 0; i < values.length; i++){ + value += values[i]; + } + text = this._getDocument(element).createTextNode(value); + element.appendChild(text); + }else{ + attribute = this._getAttribute(element.nodeName, attribute); + if(attribute.charAt(0) === '@'){ + var name = attribute.substring(1); + element.setAttribute(name, values[0]); + }else{ + for(i = element.childNodes.length - 1; i >= 0; i--){ + var node = element.childNodes[i]; + if( node.nodeType === 1 /*ELEMENT_NODE*/ && + node.nodeName === attribute){ + element.removeChild(node); + } + } + var document = this._getDocument(element); + for(i = 0; i < values.length; i++){ + child = document.createElement(attribute); + text = document.createTextNode(values[i]); + child.appendChild(text); + element.appendChild(child); + } + } + } + return true; //boolean + }, + + unsetAttribute: function(/* item */ item, /* attribute || string */ attribute){ + // summary: + // Remove an attribute + // description: + // 'item' must be an instance of a dojox.data.XmlItem from the store instance. + // 'attribute' can be an XML attribute name of the element or one of + // special names described below. + // If 'attribute' specifies "tagName", nothing is removed and false is + // returned. + // If 'attribute' specifies "childNodes" or "text()", all child nodes + // are removed. + // For generic attributes, if '_attributeMap' is specified, + // an actual attribute name is looked up with the tag name of + // the element and 'attribute' (concatenated with '.'). + // Then, if 'attribute' starts with "@", the XML attribute is removed. + // Otherwise, child elements of the tag name specified with + // 'attribute' are removed. + // item: + // An XML element that holds the attribute + // attribute: + // A tag name of child elements, an XML attribute name or one of + // special names + // returns: + // False for "tagName", otherwise true + if(attribute === "tagName"){ + return false; //boolean + } + + this._backupItem(item); + + var element = item.element; + if(attribute === "childNodes" || attribute === "text()"){ + while(element.firstChild){ + element.removeChild(element.firstChild); + } + }else{ + attribute = this._getAttribute(element.nodeName, attribute); + if(attribute.charAt(0) === '@'){ + var name = attribute.substring(1); + element.removeAttribute(name); + }else{ + for(var i = element.childNodes.length - 1; i >= 0; i--){ + var node = element.childNodes[i]; + if( node.nodeType === 1 /*ELEMENT_NODE*/ && + node.nodeName === attribute){ + element.removeChild(node); + } + } + } + } + return true; //boolean + }, + + save: function(/* object */ keywordArgs){ + // summary: + // Save new and/or modified items (XML elements) + // description: + // 'url' is used to save XML documents for new, modified and/or + // deleted XML elements. + // keywordArgs: + // An object for callbacks + if(!keywordArgs){ + keywordArgs = {}; + } + var i; + for(i = 0; i < this._modifiedItems.length; i++){ + this._saveItem(this._modifiedItems[i], keywordArgs, "PUT"); + } + for(i = 0; i < this._newItems.length; i++){ + var item = this._newItems[i]; + if(item.element.parentNode){ // reparented + this._newItems.splice(i, 1); + i--; + continue; + } + this._saveItem(this._newItems[i], keywordArgs, "POST"); + } + for(i = 0; i < this._deletedItems.length; i++){ + this._saveItem(this._deletedItems[i], keywordArgs, "DELETE"); + } + }, + + revert: function(){ + // summary: + // Invalidate changes (new and/or modified elements) + // returns: + // True + this._newItems = []; + this._restoreItems(this._deletedItems); + this._deletedItems = []; + this._restoreItems(this._modifiedItems); + this._modifiedItems = []; + return true; //boolean + }, + + isDirty: function(/* item? */ item){ + // summary: + // Check whether an item is new, modified or deleted + // description: + // If 'item' is specified, true is returned if the item is new, + // modified or deleted. + // Otherwise, true is returned if there are any new, modified + // or deleted items. + // item: + // An item (XML element) to check + // returns: + // True if an item or items are new, modified or deleted, otherwise + // false + if(item){ + var element = this._getRootElement(item.element); + return (this._getItemIndex(this._newItems, element) >= 0 || + this._getItemIndex(this._deletedItems, element) >= 0 || + this._getItemIndex(this._modifiedItems, element) >= 0); //boolean + }else{ + return (this._newItems.length > 0 || + this._deletedItems.length > 0 || + this._modifiedItems.length > 0); //boolean + } + }, + + _saveItem: function(item, keywordArgs, method){ + var url; + var scope; + if(method === "PUT"){ + url = this._getPutUrl(item); + }else if(method === "DELETE"){ + url = this._getDeleteUrl(item); + }else{ // POST + url = this._getPostUrl(item); + } + if(!url){ + if(keywordArgs.onError){ + scope = keywordArgs.scope || winUtil.global; + keywordArgs.onError.call(scope, new Error("No URL for saving content: " + this._getPostContent(item))); + } + return; + } + + var saveArgs = { + url: url, + method: (method || "POST"), + contentType: "text/xml", + handleAs: "xml" + }; + var saveHandler; + if(method === "PUT"){ + saveArgs.putData = this._getPutContent(item); + saveHandler = xhr.put(saveArgs); + }else if(method === "DELETE"){ + saveHandler = xhr.del(saveArgs); + }else{ // POST + saveArgs.postData = this._getPostContent(item); + saveHandler = xhr.post(saveArgs); + } + scope = (keywordArgs.scope || winUtil. global); + var self = this; + saveHandler.addCallback(function(data){ + self._forgetItem(item); + if(keywordArgs.onComplete){ + keywordArgs.onComplete.call(scope); + } + }); + saveHandler.addErrback(function(error){ + if(keywordArgs.onError){ + keywordArgs.onError.call(scope, error); + } + }); + }, + + _getPostUrl: function(item){ + // summary: + // Generate a URL for post + // description: + // This default implementation just returns 'url'. + // Sub-classes may override this method for the custom URL. + // item: + // An item to save + // returns: + // A post URL + return this.url; //string + }, + + _getPutUrl: function(item){ + // summary: + // Generate a URL for put + // description: + // This default implementation just returns 'url'. + // Sub-classes may override this method for the custom URL. + // item: + // An item to save + // returns: + // A put URL + return this.url; //string + }, + + _getDeleteUrl: function(item){ + // summary: + // Generate a URL for delete + // description: + // This default implementation returns 'url' with 'keyAttribute' + // as a query string. + // Sub-classes may override this method for the custom URL based on + // changes (new, deleted, or modified). + // item: + // An item to delete + // returns: + // A delete URL + var url = this.url; + if(item && this.keyAttribute !== ""){ + var value = this.getValue(item, this.keyAttribute); + if(value){ + var key = this.keyAttribute.charAt(0) ==='@' ? this.keyAttribute.substring(1): this.keyAttribute; + url += url.indexOf('?') < 0 ? '?' : '&'; + url += key + '=' + value; + } + } + return url; //string + }, + + _getPostContent: function(item){ + // summary: + // Generate a content to post + // description: + // This default implementation generates an XML document for one + // (the first only) new or modified element. + // Sub-classes may override this method for the custom post content + // generation. + // item: + // An item to save + // returns: + // A post content + return "<?xml version=\'1.0\'?>" + xmlParser.innerXML(item.element); //XML string + }, + + _getPutContent: function(item){ + // summary: + // Generate a content to put + // description: + // This default implementation generates an XML document for one + // (the first only) new or modified element. + // Sub-classes may override this method for the custom put content + // generation. + // item: + // An item to save + // returns: + // A post content + return "<?xml version='1.0'?>" + xmlParser.innerXML(item.element); //XML string + }, + +/* internal API */ + + _getAttribute: function(tagName, attribute){ + if(this._attributeMap){ + var key = tagName + "." + attribute; + var value = this._attributeMap[key]; + if(value){ + attribute = value; + }else{ // look for global attribute + value = this._attributeMap[attribute]; + if(value){ + attribute = value; + } + } + } + return attribute; //object + }, + + _getItem: function(element){ + try{ + var q = null; + //Avoid function call if possible. + if(this.keyAttribute === ""){ + q = this._getXPath(element); + } + return new XmlItem(element, this, q); //object + }catch (e){ + console.log(e); + } + return null; + }, + + _getItemIndex: function(items, element){ + for(var i = 0; i < items.length; i++){ + if(items[i].element === element){ + return i; //int + } + } + return -1; //int + }, + + _backupItem: function(item){ + var element = this._getRootElement(item.element); + if( this._getItemIndex(this._newItems, element) >= 0 || + this._getItemIndex(this._modifiedItems, element) >= 0){ + return; // new or already modified + } + if(element != item.element){ + item = this._getItem(element); + } + item._backup = element.cloneNode(true); + this._modifiedItems.push(item); + }, + + _restoreItems: function(items){ + + array.forEach(items,function(item){ + if(item._backup){ + item.element = item._backup; + item._backup = null; + } + },this); + }, + + _forgetItem: function(item){ + var element = item.element; + var index = this._getItemIndex(this._newItems, element); + if(index >= 0){ + this._newItems.splice(index, 1); + } + index = this._getItemIndex(this._deletedItems, element); + if(index >= 0){ + this._deletedItems.splice(index, 1); + } + index = this._getItemIndex(this._modifiedItems, element); + if(index >= 0){ + this._modifiedItems.splice(index, 1); + } + }, + + _getDocument: function(element){ + if(element){ + return element.ownerDocument; //DOMDocument + }else if(!this._document){ + return xmlParser.parse(); // DOMDocument + } + return null; //null + }, + + _getRootElement: function(element){ + while(element.parentNode){ + element = element.parentNode; + } + return element; //DOMElement + }, + + _getXPath: function(element){ + // summary: + // A function to compute the xpath of a node in a DOM document. + // description: + // A function to compute the xpath of a node in a DOM document. Used for + // Client side query handling and identity. + var xpath = null; + if(!this.sendQuery){ + //xpath should be null for any server queries, as we don't have the entire + //XML dom to figure it out. + var node = element; + xpath = ""; + while(node && node != element.ownerDocument){ + var pos = 0; + var sibling = node; + var name = node.nodeName; + while(sibling){ + sibling = sibling.previousSibling; + if(sibling && sibling.nodeName === name){ + pos++; + } + } + var temp = "/" + name + "[" + pos + "]"; + if(xpath){ + xpath = temp + xpath; + }else{ + xpath = temp; + } + node = node.parentNode; + } + } + return xpath; //string + }, + + /************************************* + * Dojo.data Identity implementation * + *************************************/ + getIdentity: function(/* item */ item){ + // summary: + // Returns a unique identifier for an item. + // item: + // The XML Item from the store from which to obtain its identifier. + if(!this.isItem(item)){ + throw new Error("dojox.data.XmlStore: Object supplied to getIdentity is not an item"); + }else{ + var id = null; + if(this.sendQuery && this.keyAttribute !== ""){ + id = this.getValue(item, this.keyAttribute).toString(); + }else if(!this.serverQuery){ + if(this.keyAttribute !== ""){ + id = this.getValue(item,this.keyAttribute).toString(); + }else{ + //No specified identity, so return the dojo.query/xpath + //for the node as fallback. + id = item.q; + } + } + return id; //String. + } + }, + + getIdentityAttributes: function(/* item */ item){ + // summary: + // Returns an array of attribute names that are used to generate the identity. + // description: + // For XmlStore, if sendQuery is false and no keyAttribute was set, then this function + // returns null, as xpath is used for the identity, which is not a public attribute of + // the item. If sendQuery is true and keyAttribute is set, then this function + // returns an array of one attribute name: keyAttribute. This means the server side + // implementation must apply a keyAttribute to a returned node that always allows + // it to be looked up again. + // item: + // The item from the store from which to obtain the array of public attributes that + // compose the identifier, if any. + if(!this.isItem(item)){ + throw new Error("dojox.data.XmlStore: Object supplied to getIdentity is not an item"); + }else{ + if(this.keyAttribute !== ""){ + return [this.keyAttribute]; //array + }else{ + //Otherwise it's either using xpath (not an attribute), or the remote store + //doesn't support identity. + return null; //null + } + } + }, + + + fetchItemByIdentity: function(/* object */ keywordArgs){ + // summary: + // See dojo.data.api.Identity.fetchItemByIdentity(keywordArgs) + var handleDocument = null; + var scope = null; + var self = this; + var url = null; + var getArgs = null; + var getHandler = null; + + if(!self.sendQuery){ + handleDocument = function(data){ + if(data){ + if(self.keyAttribute !== ""){ + //We have a key attribute specified. So ... we can process the items and locate the item + //that contains a matching key attribute. Its identity, as it were. + var request = {}; + request.query={}; + request.query[self.keyAttribute] = keywordArgs.identity; + request.queryOptions = {deep: true}; + var items = self._getItems(data,request); + scope = keywordArgs.scope || winUtil.global; + if(items.length === 1){ + if(keywordArgs.onItem){ + keywordArgs.onItem.call(scope, items[0]); + } + }else if(items.length === 0){ + if(keywordArgs.onItem){ + keywordArgs.onItem.call(scope, null); + } + }else{ + if(keywordArgs.onError){ + keywordArgs.onError.call(scope, new Error("Items array size for identity lookup greater than 1, invalid keyAttribute.")); + } + } + }else{ + //Since dojo.query doesn't really support the functions needed + //to do child node selection on IE well and since xpath support + //is flakey across browsers, it's simpler to implement a + //pseudo-xpath parser here. + var qArgs = keywordArgs.identity.split("/"); + var i; + var node = data; + for(i = 0; i < qArgs.length; i++){ + if(qArgs[i] && qArgs[i] !== ""){ + var section = qArgs[i]; + section = section.substring(0,section.length - 1); + var vals = section.split("["); + var tag = vals[0]; + var index = parseInt(vals[1], 10); + var pos = 0; + if(node){ + var cNodes = node.childNodes; + if(cNodes){ + var j; + var foundNode = null; + for(j = 0; j < cNodes.length; j++){ + var pNode = cNodes[j]; + if(pNode.nodeName === tag){ + if(pos < index){ + pos++; + }else{ + foundNode = pNode; + break; + } + } + } + if(foundNode){ + node = foundNode; + }else{ + node = null; + } + }else{ + node = null; + } + }else{ + break; + } + } + } + //Return what we found, if any. + var item = null; + if(node){ + item = self._getItem(node); + if(item.element.parentNode){ + item.element.parentNode.removeChild(item.element); + } + } + if(keywordArgs.onItem){ + scope = keywordArgs.scope || winUtil.global; + keywordArgs.onItem.call(scope, item); + } + } + } + }; + url = this._getFetchUrl(null); + getArgs = { + url: url, + handleAs: "xml", + preventCache: self.urlPreventCache + }; + getHandler = xhr.get(getArgs); + + //Add in the callbacks for completion of data load. + getHandler.addCallback(handleDocument); + if(keywordArgs.onError){ + getHandler.addErrback(function(error){ + var s = keywordArgs.scope || winUtil.global; + keywordArgs.onError.call(s, error); + }); + } + }else{ + //Server side querying, so need to pass the keyAttribute back to the server and let it return + //what it will. It SHOULD be only one item. + if(self.keyAttribute !== ""){ + var request = {query:{}}; + request.query[self.keyAttribute] = keywordArgs.identity; + url = this._getFetchUrl(request); + handleDocument = function(data){ + var item = null; + if(data){ + var items = self._getItems(data, {}); + if(items.length === 1){ + item = items[0]; + }else{ + if(keywordArgs.onError){ + var scope = keywordArgs.scope || winUtil.global; + keywordArgs.onError.call(scope, new Error("More than one item was returned from the server for the denoted identity")); + } + } + } + if(keywordArgs.onItem){ + scope = keywordArgs.scope || winUtil.global; + keywordArgs.onItem.call(scope, item); + } + }; + + getArgs = { + url: url, + handleAs: "xml", + preventCache: self.urlPreventCache + }; + getHandler = xhr.get(getArgs); + + //Add in the callbacks for completion of data load. + getHandler.addCallback(handleDocument); + if(keywordArgs.onError){ + getHandler.addErrback(function(error){ + var s = keywordArgs.scope || winUtil.global; + keywordArgs.onError.call(s, error); + }); + } + }else{ + if(keywordArgs.onError){ + var s = keywordArgs.scope || winUtil.global; + keywordArgs.onError.call(s, new Error("XmlStore is not told that the server to provides identity support. No keyAttribute specified.")); + } + } + } + } +}); + +lang.extend(XmlStore,simpleFetch); + +return XmlStore; +}); |
