source: [view]
define("dojox/data/XmlStore", ["dojo", "dojox", "dojo/data/util/simpleFetch", "dojo/data/util/filter", "dojox/xml/parser"], function(dojo, dojox) {
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;