]> git.pond.sub.org Git - eow/blobdiff - static/dojo-release-1.1.1/dojox/data/XmlStore.js
add Dojo 1.1.1
[eow] / static / dojo-release-1.1.1 / dojox / data / XmlStore.js
diff --git a/static/dojo-release-1.1.1/dojox/data/XmlStore.js b/static/dojo-release-1.1.1/dojox/data/XmlStore.js
new file mode 100644 (file)
index 0000000..90b26a9
--- /dev/null
@@ -0,0 +1,1141 @@
+if(!dojo._hasResource["dojox.data.XmlStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojox.data.XmlStore"] = true;
+dojo.provide("dojox.data.XmlStore");
+dojo.provide("dojox.data.XmlItem");
+
+dojo.require("dojo.data.util.simpleFetch");
+dojo.require("dojo.data.util.filter");
+dojo.require("dojox.data.dom");
+
+dojo.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 indentify
+               //              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 
+               console.log("XmlStore()");
+               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);
+               }
+               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: "",
+
+       rootItem: "",
+
+       keyAttribute: "",
+
+       label: "",
+
+       sendQuery: false,
+
+/* 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;
+               if(attribute === "tagName"){
+                       return element.nodeName;
+               }else if (attribute === "childNodes"){
+                       for (var i = 0; i < element.childNodes.length; i++) {
+                               var node = element.childNodes[i];
+                               if (node.nodeType === 1 /*ELEMENT_NODE*/) {
+                                       return this._getItem(node); //object
+                               }
+                       }
+                       return defaultValue;
+               }else if(attribute === "text()"){
+                       for(var i = 0; i < element.childNodes.length; i++){
+                               var 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);
+                               return (value !== undefined) ? value : defaultValue; //object
+                       }else{
+                               for(var i = 0; i < element.childNodes.length; i++){
+                                       var 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;
+               if(attribute === "tagName"){
+                       return [element.nodeName];
+               }else if(attribute === "childNodes"){
+                       var values = [];
+                       for(var i = 0; i < element.childNodes.length; i++){
+                               var node = element.childNodes[i];
+                               if(node.nodeType === 1 /*ELEMENT_NODE*/){
+                                       values.push(this._getItem(node));
+                               }
+                       }
+                       return values; //array
+               }else if(attribute === "text()"){
+                       var values = [];
+                       for(var i = 0; i < element.childNodes.length; i++){
+                               var node = childNodes[i];
+                               if(node.nodeType === 3){
+                                       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{
+                               var values = [];
+                               for(var i = 0; i < element.childNodes.length; i++){
+                                       var 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 = [];
+               attributes.push("tagName");
+               if(element.childNodes.length > 0){
+                       var names = {};
+                       var childNodes = true;
+                       var text = false;
+                       for(var 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(var i = 0; i < element.attributes.length; i++){
+                       attributes.push("@" + element.attributes[i].nodeName);
+               }
+               if(this._attributeMap){
+                       for (var key in this._attributeMap){
+                               var 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
+               };
+               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);
+               console.log("XmlStore._fetchItems(): url=" + url);
+               if(!url){
+                       errorHandler(new Error("No URL specified."));
+                       return;
+               }
+               var localRequest = (!this.sendQuery ? request : null); // use request for _getItems()
+
+               var self = this;
+               var getArgs = {
+                               url: url,
+                               handleAs: "xml",
+                               preventCache: true
+                       };
+               var getHandler = dojo.xhrGet(getArgs);
+               getHandler.addCallback(function(data){
+                       var items = self._getItems(data, localRequest);
+                       console.log("XmlStore._fetchItems(): length=" + (items ? items.length : 0));
+                       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(dojo.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;
+
+               console.log("Looking up root item: " + this.rootItem);
+               if(this.rootItem !== ""){
+                       
+                       nodes = document.getElementsByTagName(this.rootItem);
+               }
+               else{
+                       nodes = document.documentElement.childNodes;
+               }
+               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 found = true;
+                               var ignoreCase = request.queryOptions ? request.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 query){
+                                       var value = query[key];
+                                       if(typeof value === "string"){
+                                               regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
+                                       }
+                               }
+
+                               for(var attribute in query){
+                                       var value = this.getValue(item, attribute);
+                                       if(value){
+                                               var queryValue = query[attribute];
+                                               if ((typeof value) === "string" && 
+                                                       (regexpList[attribute])){
+                                                       if((value.match(regexpList[attribute])) !== null){
+                                                               continue;
+                                                       }
+                                               }else if((typeof value) === "object"){
+                                                       if(     value.toString && 
+                                                               (regexpList[attribute])){
+                                                               var stringValue = value.toString();
+                                                               if((stringValue.match(regexpList[attribute])) !== null){
+                                                                       continue;
+                                                               }
+                                                       }else{
+                                                               if(queryValue === "*" || queryValue === value){
+                                                                       continue;
+                                                               }
+                                                       }
+                                               }
+                                       }
+                                       found = false;
+                                       break;
+                               }
+                               if(!found){
+                                       continue;
+                               }
+                       }
+                       items.push(item);
+               }
+               dojo.forEach(items,function(item){ 
+                       item.element.parentNode.removeChild(item.element); // make it root
+               },this); 
+               return items;
+       },
+
+       close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
+                //     summary: 
+                //             See dojo.data.api.Read.close()
+       },
+
+/* dojo.data.api.Write */
+
+       newItem: function(/* object? */ keywordArgs){
+               //      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
+               console.log("XmlStore.newItem()");
+               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){
+                       if(attribute === "tagName"){
+                               continue;
+                       }else if(attribute === "text()"){
+                               var 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);
+                                       var text = document.createTextNode(keywordArgs[attribute]);
+                                       child.appendChild(text);
+                                       element.appendChild(child);
+                               }
+                       }
+               }
+
+               var item = this._getItem(element);
+               this._newItems.push(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
+               console.log("XmlStore.deleteItem()");
+               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;
+               if(attribute === "childNodes"){
+                       var child = value.element;
+                       element.appendChild(child);
+               }else if(attribute === "text()"){
+                       while (element.firstChild){
+                               element.removeChild(element.firstChild);
+                       }
+                       var 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{
+                               var child = null;
+                               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);
+                               }
+                               var 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
+               //      returns:
+               //              False for "tagName", otherwise true
+               if(attribute === "tagName"){
+                       return false; //boolean
+               }
+
+               this._backupItem(item);
+
+               var element = item.element;
+               if(attribute === "childNodes"){
+                       while(element.firstChild){
+                               element.removeChild(element.firstChild);
+                       }
+                       for(var i = 0; i < values.length; i++){
+                               var child = values[i].element;
+                               element.appendChild(child);
+                       }
+               }else if(attribute === "text()"){
+                       while (element.firstChild){
+                               element.removeChild(element.firstChild);
+                       }
+                       var value = "";
+                       for(var i = 0; i < values.length; i++){
+                               value += values[i];
+                       }
+                       var 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(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);
+                                       }
+                               }
+                               var document = this._getDocument(element);
+                               for(var i = 0; i < values.length; i++){
+                                       var child = document.createElement(attribute);
+                                       var 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 = {};
+               }
+               for(var i = 0; i < this._modifiedItems.length; i++){
+                       this._saveItem(this._modifiedItems[i], keywordArgs, "PUT");
+               }
+               for(var 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(var 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
+               console.log("XmlStore.revert() _newItems=" + this._newItems.length);
+               console.log("XmlStore.revert() _deletedItems=" + this._deletedItems.length);
+               console.log("XmlStore.revert() _modifiedItems=" + this._modifiedItems.length);
+               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){
+               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){
+                               keywordArgs.onError.call(scope, new Error("No URL for saving content: " + postContent));
+                       }
+                       return;
+               }
+
+               var saveArgs = {
+                       url: url,
+                       method: (method || "POST"),
+                       contentType: "text/xml",
+                       handleAs: "xml"
+               };
+               var saveHander;
+               if(method === "PUT"){
+                       saveArgs.putData = this._getPutContent(item);
+                       saveHandler = dojo.rawXhrPut(saveArgs);
+               }else if(method === "DELETE"){
+                       saveHandler = dojo.xhrDelete(saveArgs);
+               }else{ // POST
+                       saveArgs.postData = this._getPostContent(item);
+                       saveHandler = dojo.rawXhrPost(saveArgs);
+               }
+               var scope = (keywordArgs.scope || dojo.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
+               var element = item.element;
+               var declaration = "<?xml version=\"1.0\"?>"; // FIXME: encoding?
+               return declaration + dojox.data.dom.innerXML(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
+               var element = item.element;
+               var declaration = "<?xml version=\"1.0\"?>"; // FIXME: encoding?
+               return declaration + dojox.data.dom.innerXML(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){
+               return new dojox.data.XmlItem(element, this); //object
+       },
+
+       _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){
+
+               dojo.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 dojox.data.dom.createDocument(); // DOMDocument
+               }
+       },
+
+       _getRootElement: function(element){
+               while(element.parentNode){
+                       element = element.parentNode;
+               }
+               return element; //DOMElement
+       }
+
+});
+
+//FIXME: Is a full class here really needed for containment of the item or would
+//an anon object work fine?
+dojo.declare("dojox.data.XmlItem", null, {
+       constructor: function(element, store) {
+               //      summary:
+               //              Initialize with an XML element
+               //      element:
+               //              An XML element
+               //      store:
+               //              The containing store, if any.
+               this.element = element;
+               this.store = store;
+       }, 
+       //      summary:
+       //              A data item of 'XmlStore'
+       //      description:
+       //              This class represents an item of 'XmlStore' holding an XML element.
+       //              'element'
+       //      element:
+       //              An XML element
+
+       toString: function() {
+               //      summary:
+               //              Return a value of the first text child of the element
+               //      returns:
+               //              a value of the first text child of the element
+               var str = "";
+               if (this.element) {
+                       for (var i = 0; i < this.element.childNodes.length; i++) {
+                               var node = this.element.childNodes[i];
+                               if (node.nodeType === 3) {
+                                       str = node.nodeValue;
+                                       break;
+                               }
+                       }
+               }
+               return str;     //String
+       }
+
+});
+dojo.extend(dojox.data.XmlStore,dojo.data.util.simpleFetch);
+
+}