/*
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.CdfStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.data.CdfStore"] = true;
dojo.provide("dojox.data.CdfStore");
dojo.require("dojo.data.util.sorter");
dojox.data.ASYNC_MODE = 0;
dojox.data.SYNC_MODE = 1;
dojo.declare("dojox.data.CdfStore", null, {
// summary:
// IMPORTANT: The CDF Store is designed to work with Tibco GI, and references Tibco's
// JSX3 JavaScript library and will not work without it.
//
// The CDF Store implements dojo.data.Read, Write, and Identity api's. It is a local
// (in memory) store that handles XML documents formatted according to the
// Common Data Format (CDF) spec:
// http://www.tibco.com/devnet/resources/gi/3_1/tips_and_techniques/CommonDataFormatCDF.pdf
//
// The purpose of this store is to provide a glue between a jsx3 CDF file and a Dijit.
//
// While a CDF document is an XML file, other than the initial input, all data returned
// from and written to this store should be in object format.
//
// identity: [const] String
// The unique identifier for each item. Defaults to "jsxid" which is standard for a CDF
// document. Should not be changed.
identity: "jsxid",
//
// url : String
// The location from which to fetch the XML (CDF) document.
url: "",
//
// xmlStr: String
// A string that can be parsed into an XML document and should be formatted according
// to the CDF spec.
// example:
// | '<data jsxid="jsxroot"><record jsxtext="A"/><record jsxtext="B" jsxid="2" jsxid="2"/></data>'
xmlStr:"",
//
// data: Object
// A object that will be converted into the xmlStr property, and then parsed into a CDF.
data:null,
//
// label: String
// The property within each item used to define the item.
label: "",
//
// mode [const]: dojox.data.ASYNC_MODE | dojox.data.SYNC_MODE
// This store supports syncronous fetches if this property is set to dojox.data.SYNC_MODE.
mode:dojox.data.ASYNC_MODE,
constructor: function(/* Object */ args){
// summary:
// Constructor for the CDF store. Instantiate a new CdfStore.
//
if(args){
this.url = args.url;
this.xmlStr = args.xmlStr || args.str;
if(args.data){
this.xmlStr = this._makeXmlString(args.data);
}
this.identity = args.identity || this.identity;
this.label = args.label || this.label;
this.mode = args.mode !== undefined ? args.mode : this.mode;
}
this._modifiedItems = {};
this.byId = this.fetchItemByIdentity;
},
/* dojo.data.api.Read */
getValue: function(/* jsx3.xml.Entity */ item, /* String */ property, /* value? */ defaultValue){
// summary:
// Return an property value of an item
//
return item.getAttribute(property) || defaultValue; // anything
},
getValues: function(/* jsx3.xml.Entity */ item, /* String */ property){
// summary:
// Return an array of values
//
// TODO!!! Can't find an example of an array in any CDF files
//
var v = this.getValue(item, property, []);
return dojo.isArray(v) ? v : [v];
},
getAttributes: function(/* jsx3.xml.Entity */ item){
// summary:
// Return an array of property names
//
return item.getAttributeNames(); // Array
},
hasAttribute: function(/* jsx3.xml.Entity */ item, /* String */ property){
// summary:
// Check whether an item has a property
//
return (this.getValue(item, property) !== undefined); // Boolean
},
hasProperty: function(/* jsx3.xml.Entity */ item, /* String */ property){
// summary:
// Alias for hasAttribute
return this.hasAttribute(item, property);
},
containsValue: function(/* jsx3.xml.Entity */ item, /* String */ property, /* anything */ value){
// summary:
// Check whether an item contains a value
//
var values = this.getValues(item, property);
for(var i = 0; i < values.length; i++){
if(values[i] === null){ continue; }
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 (jsx3.xml.Entity)
//
if(something.getClass && something.getClass().equals(jsx3.xml.Entity.jsxclass)){
return true; //boolean
}
return false; //boolran
},
isItemLoaded: function(/* anything */ something){
// summary:
// Check whether the object is a jsx3.xml.Entity object and loaded
//
return this.isItem(something); // Boolean
},
loadItem: function(/* object */ keywordArgs){
// summary:
// Load an item
// description:
// The store always loads all items, so if it's an item, then it's loaded.
},
getFeatures: function(){
// summary:
// Return supported data APIs
//
return {
"dojo.data.api.Read": true,
"dojo.data.api.Write": true,
"dojo.data.api.Identity":true
}; // Object
},
getLabel: function(/* jsx3.xml.Entity */ 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(/* jsx3.xml.Entity */ item){
// summary:
// returns an array of what properties of the item that were used
// to generate its label
// See dojo.data.api.Read.getLabelAttributes()
//
if(this.label !== ""){
return [this.label]; //array
}
return null; //null
},
fetch: function(/* Object? */ request){
// summary:
// Returns an Array of items based on the request arguments.
// description:
// Returns an Array of items based on the request arguments.
// If the store is in ASYNC mode, the items should be expected in an onComplete
// method passed in the request object. If store is in SYNC mode, the items will
// be return directly as well as within the onComplete method.
// note:
// The mode can be set on store initialization or during a fetch as one of the
// parameters.
//
// query: String
// The items in the store are treated as objects, but this is reading an XML
// document. Further, the actual querying of the items takes place in Tibco GI's
// jsx3.xml.Entity. Therefore, we are using their syntax which is xpath.
// Note:
// As conforming to a CDF document, most, if not all nodes are considered "records"
// and their tagNames are as such. The root node is named "data".
//
// examples:
// All items:
// | store.fetch({query:"*"});
// Item with a jsxid attribute equal to "1" (note you could use byId for this)
// | store.fetch({query:"//record[@jsxid='1']"});
// All items with any jsxid attribute:
// | "//record[@jsxid='*']"
// The items with a jsxid of '1' or '4':
// | "//record[@jsxid='4' or @jsxid='1']"
// All children within a "group" node (could be multiple group nodes):
// "//group/record"
// All children within a specific group node:
// "//group[@name='mySecondGroup']/record"
// Any record, anywhere in the document:
// | "//record"
// Only the records beneath the root (data) node:
// | "//data/record"
//
// See:
// http://www.tibco.com/devnet/resources/gi/3_7/api/html/jsx3/xml/Entity.html#method:selectNodes
// http://www.w3.org/TR/xpath
// http://msdn.microsoft.com/en-us/library/ms256086.aspx
//
// See dojo.data.Read.fetch():
// onBegin
// onComplete
// onItem
// onError
// scope
// start
// count
// sort
//
request = request || {};
if(!request.store){
request.store = this;
}
if(request.mode !== undefined){
this.mode = request.mode;
}
var self = this;
var errorHandler = function(errorData){
if(request.onError){
var scope = request.scope || dojo.global;
request.onError.call(scope, errorData, request);
}else{
console.error("cdfStore Error:", errorData);
}
};
var fetchHandler = function(items, requestObject){
requestObject = requestObject || request;
var oldAbortFunction = requestObject.abort || null;
var aborted = false;
var startIndex = requestObject.start?requestObject.start:0;
var endIndex = (requestObject.count && (requestObject.count !== Infinity))?(startIndex + requestObject.count):items.length;
requestObject.abort = function(){
aborted = true;
if(oldAbortFunction){
oldAbortFunction.call(requestObject);
}
};
var scope = requestObject.scope || dojo.global;
if(!requestObject.store){
requestObject.store = self;
}
if(requestObject.onBegin){
requestObject.onBegin.call(scope, items.length, requestObject);
}
if(requestObject.sort){
items.sort(dojo.data.util.sorter.createSortFunction(requestObject.sort, self));
}
if(requestObject.onItem){
for(var i = startIndex; (i < items.length) && (i < endIndex); ++i){
var item = items[i];
if(!aborted){
requestObject.onItem.call(scope, item, requestObject);
}
}
}
if(requestObject.onComplete && !aborted){
if(!requestObject.onItem){
items = items.slice(startIndex, endIndex);
if(requestObject.byId){
items = items[0];
}
}
requestObject.onComplete.call(scope, items, requestObject);
}else{
items = items.slice(startIndex, endIndex);
if(requestObject.byId){
items = items[0];
}
}
return items;
};
if(!this.url && !this.data && !this.xmlStr){
errorHandler(new Error("No URL or data specified."));
return false;
}
var localRequest = request || "*"; // use request for _getItems()
if(this.mode == dojox.data.SYNC_MODE){
// sync mode. items returned directly
var res = this._loadCDF();
if(res instanceof Error){
if(request.onError){
request.onError.call(request.scope || dojo.global, res, request);
}else{
console.error("CdfStore Error:", res);
}
return res;
}
this.cdfDoc = res;
var items = this._getItems(this.cdfDoc, localRequest);
if(items && items.length > 0){
items = fetchHandler(items, request);
}else{
items = fetchHandler([], request);
}
return items;
}else{
// async mode. Return a Deferred.
var dfd = this._loadCDF();
dfd.addCallbacks(dojo.hitch(this, function(cdfDoc){
var items = this._getItems(this.cdfDoc, localRequest);
if(items && items.length > 0){
fetchHandler(items, request);
}else{
fetchHandler([], request);
}
}),
dojo.hitch(this, function(err){
errorHandler(err, request);
}));
return dfd; // Object
}
},
_loadCDF: function(){
// summary:
// Internal method.
// If a cdfDoc exists, return it. Otherwise, get one from JSX3,
// load the data or url, and return the doc or a deferred.
var dfd = new dojo.Deferred();
if(this.cdfDoc){
if(this.mode == dojox.data.SYNC_MODE){
return this.cdfDoc; // jsx3.xml.CDF
}else{
setTimeout(dojo.hitch(this, function(){
dfd.callback(this.cdfDoc);
}), 0);
return dfd; // dojo.Deferred
}
}
this.cdfDoc = jsx3.xml.CDF.Document.newDocument();
this.cdfDoc.subscribe("response", this, function(evt){
dfd.callback(this.cdfDoc);
});
this.cdfDoc.subscribe("error", this, function(err){
dfd.errback(err);
});
this.cdfDoc.setAsync(!this.mode);
if(this.url){
this.cdfDoc.load(this.url);
}else if(this.xmlStr){
this.cdfDoc.loadXML(this.xmlStr);
if(this.cdfDoc.getError().code){
return new Error(this.cdfDoc.getError().description); // Error
}
}
if(this.mode == dojox.data.SYNC_MODE){
return this.cdfDoc; // jsx3.xml.CDF
}else{
return dfd; // dojo.Deferred
}
},
_getItems: function(/* jsx3.xml.Entity */cdfDoc, /* Object */request){
// summary:
// Internal method.
// Requests the items from jsx3.xml.Entity with an xpath query.
//
var itr = cdfDoc.selectNodes(request.query, false, 1);
var items = [];
while(itr.hasNext()){
items.push(itr.next());
}
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, /* object? || String? */parentInfo){
// summary:
// Creates a jsx3.xml.Entity item and inserts it either inside the
// parent or appends it to the root
//
keywordArgs = (keywordArgs || {});
if(keywordArgs.tagName){
// record tagName is automatic and this would add it
// as a property
if(keywordArgs.tagName!="record"){
// TODO: How about some sort of group?
console.warn("Only record inserts are supported at this time");
}
delete keywordArgs.tagName;
}
keywordArgs.jsxid = keywordArgs.jsxid || this.cdfDoc.getKey();
if(this.isItem(parentInfo)){
parentInfo = this.getIdentity(parentInfo);
}
var item = this.cdfDoc.insertRecord(keywordArgs, parentInfo);
this._makeDirty(item);
return item; // jsx3.xml.Entity
},
deleteItem: function(/* jsx3.xml.Entity */ item){
// summary:
// Delete an jsx3.xml.Entity (wrapper to a XML element).
//
this.cdfDoc.deleteRecord(this.getIdentity(item));
this._makeDirty(item);
return true; //boolean
},
setValue: function(/* jsx3.xml.Entity */ item, /* String */ property, /* almost anything */ value){
// summary:
// Set an property value
//
this._makeDirty(item);
item.setAttribute(property, value);
return true; // Boolean
},
setValues: function(/* jsx3.xml.Entity */ item, /* String */ property, /*array*/ values){
// summary:
// Set property values
// TODO: Needs to be fully implemented.
//
this._makeDirty(item);
console.warn("cdfStore.setValues only partially implemented.");
return item.setAttribute(property, values);
},
unsetAttribute: function(/* jsx3.xml.Entity */ item, /* String */ property){
// summary:
// Remove an property
//
this._makeDirty(item);
item.removeAttribute(property);
return true; // Boolean
},
revert: function(){
// summary:
// Invalidate changes (new and/or modified elements)
// Resets data by simply deleting the reference to the cdfDoc.
// Subsequent fetches will load the new data.
// Note:
// Any items outside the store will no longer be valid and may cause errors.
//
delete this.cdfDoc;
this._modifiedItems = {};
return true; //boolean
},
isDirty: function(/* jsx3.xml.Entity ? */ item){
// summary:
// Check whether an item is new, modified or deleted.
// If no item is passed, checks if anything in the store has changed.
//
if(item){
return !!this._modifiedItems[this.getIdentity(item)]; // Boolean
}else{
var _dirty = false;
for(var nm in this._modifiedItems){ _dirty = true; break; }
return _dirty; // Boolean
}
},
/* internal API */
_makeDirty: function(item){
// summary:
// Internal method.
// Marks items as modified, deleted or new.
var id = this.getIdentity(item);
this._modifiedItems[id] = item;
},
_makeXmlString: function(obj){
// summary:
// Internal method.
// Converts an object into an XML string.
//
var parseObj = function(obj, name){
var xmlStr = "";
var nm;
if(dojo.isArray(obj)){
for(var i=0;i<obj.length;i++){
xmlStr += parseObj(obj[i], name);
}
}else if(dojo.isObject(obj)){
xmlStr += '<'+name+' ';
for(nm in obj){
if(!dojo.isObject(obj[nm])){
xmlStr += nm+'="'+obj[nm]+'" ';
}
}
xmlStr +='>';
for(nm in obj){
if(dojo.isObject(obj[nm])){
xmlStr += parseObj(obj[nm], nm);
}
}
xmlStr += '</'+name+'>';
}
return xmlStr;
};
return parseObj(obj, "data");
},
/*************************************
* Dojo.data Identity implementation *
*************************************/
getIdentity: function(/* jsx3.xml.Entity */ item){
// summary:
// Returns the identifier for an item.
//
return this.getValue(item, this.identity); // String
},
getIdentityAttributes: function(/* jsx3.xml.Entity */ item){
// summary:
// Returns the property used for the identity.
//
return [this.identity]; // Array
},
fetchItemByIdentity: function(/* Object || String */ args){
// summary:
// See dojo.data.api.Identity.fetchItemByIdentity(keywordArgs)
//
// Note:
// This method can be synchronous if mode is set.
// Also, there is a more finger friendly alias of this method, byId();
if(dojo.isString(args)){
var id = args;
args = {query:"//record[@jsxid='"+id+"']", mode: dojox.data.SYNC_MODE};
}else{
if(args){
args.query = "//record[@jsxid='"+args.identity+"']";
}
if(!args.mode){args.mode = this.mode;}
}
args.byId = true;
return this.fetch(args); // dojo.Deferred || Array
},
byId: function(/* Object || String */ args){
// stub. See fetchItemByIdentity
}
});
}