/*
Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved.
Available via Academic Free License >= 2.1 OR the modified BSD license.
see: http://dojotoolkit.org/license for details
*/
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.require("dojo.data.util.simpleFetch");
dojo.require("dojo.data.util.filter");
dojo.require("dojox.xml.parser");
dojo.provide("dojox.data.XmlItem");
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 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);
console.log("XmlStore._fetchItems(): url=" + url);
if(!url){
errorHandler(new Error("No URL specified."));
return;
}
var localRequest = (!this.sendQuery ? request : {}); // use request for _getItems()
var self = this;
var getArgs = {
url: url,
handleAs: "xml",
preventCache: self.urlPreventCache
};
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;
if(this.rootItem !== ""){
nodes = dojo.query(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] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
}
}
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);
}
}
dojo.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
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){
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.setValues(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
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;
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
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){
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 || dojo.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 = dojo.rawXhrPut(saveArgs);
}else if(method === "DELETE"){
saveHandler = dojo.xhrDelete(saveArgs);
}else{ // POST
saveArgs.postData = this._getPostContent(item);
saveHandler = dojo.rawXhrPost(saveArgs);
}
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.xml.parser.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.xml.parser.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){
try{
var q = null;
//Avoid function call if possible.
if(this.keyAttribute === ""){
q = this._getXPath(element);
}
return new dojox.data.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){
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.xml.parser.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 || dojo.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 || dojo.global;
keywordArgs.onItem.call(scope, item);
}
}
}
};
url = this._getFetchUrl(null);
getArgs = {
url: url,
handleAs: "xml",
preventCache: self.urlPreventCache
};
getHandler = dojo.xhrGet(getArgs);
//Add in the callbacks for completion of data load.
getHandler.addCallback(handleDocument);
if(keywordArgs.onError){
getHandler.addErrback(function(error){
var s = keywordArgs.scope || dojo.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 || dojo.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 || dojo.global;
keywordArgs.onItem.call(scope, item);
}
};
getArgs = {
url: url,
handleAs: "xml",
preventCache: self.urlPreventCache
};
getHandler = dojo.xhrGet(getArgs);
//Add in the callbacks for completion of data load.
getHandler.addCallback(handleDocument);
if(keywordArgs.onError){
getHandler.addErrback(function(error){
var s = keywordArgs.scope || dojo.global;
keywordArgs.onError.call(s, error);
});
}
}else{
if(keywordArgs.onError){
var s = keywordArgs.scope || dojo.global;
keywordArgs.onError.call(s, new Error("XmlStore is not told that the server to provides identity support. No keyAttribute specified."));
}
}
}
}
});
dojo.declare("dojox.data.XmlItem", null, {
constructor: function(element, store, query){
// summary:
// Initialize with an XML element
// element:
// An XML element
// store:
// The containing store, if any.
// query:
// The query to use to look up a specific element.
// Usually an XPath or dojo.query statement.
this.element = element;
this.store = store;
this.q = query;
},
// 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 || node.nodeType === 4){
str += node.nodeValue;
}
}
}
return str; //String
}
});
dojo.extend(dojox.data.XmlStore,dojo.data.util.simpleFetch);
}