Location: A review of cardiac cellular electrophysiology models @ c47db6b2fedb / dojo-presentation / js / dojo / dojox / data / jsonPathStore.js

Author:
David Nickerson <david.nickerson@gmail.com>
Date:
2021-09-17 15:39:51+12:00
Desc:
tweak html formatting
Permanent Source URI:
https://models.fieldml.org/workspace/a1/rawfile/c47db6b2fedb368422c7f4d5191aeb9f319ad684/dojo-presentation/js/dojo/dojox/data/jsonPathStore.js

dojo.provide("dojox.data.jsonPathStore");
dojo.require("dojox.jsonPath");
dojo.require("dojo.date");
dojo.require("dojo.date.locale");
dojo.require("dojo.date.stamp");

dojox.data.ASYNC_MODE = 0;
dojox.data.SYNC_MODE = 1;

dojo.declare("dojox.data.jsonPathStore",
	null,
	{
		mode: dojox.data.ASYNC_MODE,
		metaLabel: "_meta",
		hideMetaAttributes: false,
		autoIdPrefix: "_auto_",
		autoIdentity: true,
		idAttribute: "_id",
		indexOnLoad: true,
		labelAttribute: "",
		url: "",
		_replaceRegex: /\'\]/gi,
		
		constructor: function(options){
			//summary:
			//	jsonPathStore constructor, instantiate a new jsonPathStore 
			//
			//	Takes a single optional parameter in the form of a Javascript object
			//	containing one or more of the following properties. 
			//
			//	data: /*JSON String*/ || /* Javascript Object */, 
			//		JSON String or Javascript object this store will control
			//		JSON is converted into an object, and an object passed to
			//		the store will be used directly.  If no data and no url
			//		is provide, an empty object, {}, will be used as the initial
			//		store.
			//
			//	url: /* string url */ 	
			//		Load data from this url in JSON format and use the Object
			//		created from the data as the data source.
			//
			//	indexOnLoad: /* boolean */ 
			//		Defaults to true, but this may change in the near future.
			//		Parse the data object and set individual objects up as
			//		appropriate.  This will add meta data and assign
			//		id's to objects that dont' have them as defined by the
			//		idAttribute option.  Disabling this option will keep this 
			//		parsing from happening until a query is performed at which
			//		time only the top level of an item has meta info stored.
			//		This might work in some situations, but you will almost
			//		always want to indexOnLoad or use another option which
			//		will create an index.  In the future we will support a 
			//		generated index that maps by jsonPath allowing the
			//		server to take some of this load for larger data sets. 
			//
			//	idAttribute: /* string */
			//		Defaults to '_id'. The name of the attribute that holds an objects id.
			//		This can be a preexisting id provided by the server.  
			//		If an ID isn't already provided when an object
			//		is fetched or added to the store, the autoIdentity system
			//		will generate an id for it and add it to the index. There
			//		are utility routines for exporting data from the store
			//		that can clean any generated IDs before exporting and leave
			//		preexisting id's in tact.
			//
			//	metaLabel: /* string */
			//		Defaults to '_meta' overrides the attribute name that is used by the store
			//		for attaching meta information to an object while
			//		in the store's control.  Defaults to '_meta'. 
			//	
			//	hideMetaAttributes: /* boolean */
			//		Defaults to False.  When enabled, calls to getAttributes() will not 
			//		include the meta attribute.
			//
			//	autoIdPrefix: /*string*/
			//		Defaults to "_auto_".  This string is used as the prefix to any
			//		objects which have a generated id. A numeric index is appended
			//		to this string to complete the ID
			//
			//	mode: dojox.data.ASYNC_MODE || dojox.data.SYNC_MODE
			//		Defaults to ASYNC_MODE.  This option sets the default mode for this store.
			//		Sync calls return their data immediately from the calling function
			//		instead of calling the callback functions.  Functions such as 
			//		fetchItemByIdentity() and fetch() both accept a string parameter in addtion
			//		to the normal keywordArgs parameter.  When passed this option, SYNC_MODE will
			//		automatically be used even when the default mode of the system is ASYNC_MODE.
			//		A normal request to fetch or fetchItemByIdentity (with kwArgs object) can also 
			//		include a mode property to override this setting for that one request.

			//setup a byId alias to the api call	
			this.byId=this.fetchItemByIdentity;

			if (options){
				dojo.mixin(this,options);
			}

			this._dirtyItems=[];
			this._autoId=0;
			this._referenceId=0;
			this._references={};
			this._fetchQueue=[];
			this.index={};

			//regex to identify when we're travelling down metaObject (which we don't want to do) 
			var expr="("+this.metaLabel+"\'\])";
			this.metaRegex = new RegExp(expr);


			//no data or url, start with an empty object for a store
			if (!this.data && !this.url){
				this.setData({});
			}	

			//we have data, but no url, set the store as the data
			if (this.data && !this.url){
				this.setData(this.data);

				//remove the original refernce, we're now using _data from here on out
				delete this.data;
			}

			//given a url, load json data from as the store
			if (this.url){
				dojo.xhrGet({
					url: options.url,
					handleAs: "json",
					load: dojo.hitch(this, "setData"),
					sync: this.mode
				});
			}
		},

		_loadData: function(data){
			// summary:
			//	load data into the store. Index it if appropriate.
			if (this._data){
				delete this._data;
			}

			if (dojo.isString(data)){
				this._data = dojo.fromJson(data);
			}else{
				this._data = data;
			}
			
			if (this.indexOnLoad){
				this.buildIndex();		
			}	

			this._updateMeta(this._data, {path: "$"});

			this.onLoadData(this._data);
		},

		onLoadData: function(data){
			// summary
			//	Called after data has been loaded in the store.  
			//	If any requests happened while the startup is happening
			//	then process them now.

			while (this._fetchQueue.length>0){
				var req = this._fetchQueue.shift();
				this.fetch(req);
			}	

		},

		setData: function(data){
			// summary:
			//	set the stores' data to the supplied object and then 
			//	load and/or setup that data with the required meta info		
			this._loadData(data);
		},

		buildIndex: function(path, item){
			//summary: 
			//	parse the object structure, and turn any objects into
			//	jsonPathStore items. Basically this just does a recursive
			//	series of fetches which itself already examines any items
			//	as they are retrieved and setups up the required meta information. 
			//
			//	path: /* string */
			//		jsonPath Query for the starting point of this index construction.

			if (!this.idAttribute){
				throw new Error("buildIndex requires idAttribute for the store");
			}

			item = item || this._data;
			var origPath = path;
			path = path||"$";
			path += "[*]";
			var data = this.fetch({query: path,mode: dojox.data.SYNC_MODE});
			for(var i=0; i<data.length;i++){
				if(dojo.isObject(data[i])){
					var newPath = data[i][this.metaLabel]["path"];
					if (origPath){
						//console.log("newPath: ", newPath);
						//console.log("origPath: ", origPath);
						//console.log("path: ", path);
						//console.log("data[i]: ", data[i]);
						var parts = origPath.split("\[\'");
						var attribute = parts[parts.length-1].replace(this._replaceRegex,'');
						//console.log("attribute: ", attribute);
						//console.log("ParentItem: ", item, attribute);
						if (!dojo.isArray(data[i])){
							this._addReference(data[i], {parent: item, attribute:attribute});
							this.buildIndex(newPath, data[i]);
						}else{
							this.buildIndex(newPath,item);
						}
					}else{
						var parts = newPath.split("\[\'");
						var attribute = parts[parts.length-1].replace(this._replaceRegex,'');
						this._addReference(data[i], {parent: this._data, attribute:attribute});
						this.buildIndex(newPath, data[i]);
					}
				}
			}
		},

		_correctReference: function(item){
			// summary:
			//	make sure we have an reference to the item in the store
			//	and not a clone. Takes an item, matches it to the corresponding
			//	item in the store and if it is the same, returns itself, otherwise
			//	it returns the item from the store.
		
			if (this.index[item[this.idAttribute]][this.metaLabel]===item[this.metaLabel]){
				return this.index[item[this.idAttribute]];
			}
			return item;	
		},

		getValue: function(item, property){
			// summary:
			//	Gets the value of an item's 'property'
			//
			//	item: /* object */
			//	property: /* string */
			//		property to look up value for	
			item = this._correctReference(item);
			return item[property];
		},

		getValues: function(item, property){
			// summary:
			//	Gets the value of an item's 'property' and returns
			//	it.  If this value is an array it is just returned,
			//	if not, the value is added to an array and that is returned.
			//
			//	item: /* object */
			//	property: /* string */
			//		property to look up value for	
	
			item = this._correctReference(item);
			return dojo.isArray(item[property]) ? item[property] : [item[property]];
		},

		getAttributes: function(item){
			// summary:
			//	Gets the available attributes of an item's 'property' and returns
			//	it as an array. If the store has 'hideMetaAttributes' set to true
			//	the attributed identified by 'metaLabel' will not be included.
			//
			//	item: /* object */

			item = this._correctReference(item);
			var res = [];
			for (var i in item){
				if (this.hideMetaAttributes && (i==this.metaLabel)){continue;}
				res.push(i);
			}
			return res;
		},

		hasAttribute: function(item,attribute){
			// summary:
			//	Checks to see if item has attribute
			//
			//	item: /* object */
			//	attribute: /* string */
		
			item = this._correctReference(item);
			if (attribute in item){return true;}
			return false;	
		},

		containsValue: function(item, attribute, value){
			// summary:
			//	Checks to see if 'item' has 'value' at 'attribute'
			//
			//	item: /* object */
			//	attribute: /* string */
			//	value: /* anything */
			item = this._correctReference(item);

			if (item[attribute] && item[attribute]==value){return true}
			if (dojo.isObject(item[attribute]) || dojo.isObject(value)){
				if (this._shallowCompare(item[attribute],value)){return true}
			}
			return false;	
		},

		_shallowCompare: function(a, b){
			//summary does a simple/shallow compare of properties on an object
			//to the same named properties on the given item. Returns
			//true if all props match. It will not descend into child objects
			//but it will compare child date objects

			if ((dojo.isObject(a) && !dojo.isObject(b))|| (dojo.isObject(b) && !dojo.isObject(a))) {
				return false;
			}

			if ( a["getFullYear"] || b["getFullYear"] ){
				//confirm that both are dates
				if ( (a["getFullYear"] && !b["getFullYear"]) || (b["getFullYear"] && !a["getFullYear"]) ){
					return false;
				}else{
					if (!dojo.date.compare(a,b)){
						return true;
					}
					return false;
       				}
			}

			for (var i in b){	
				if (dojo.isObject(b[i])){
					if (!a[i] || !dojo.isObject(a[i])){return false}

					if (b[i]["getFullYear"]){
						if(!a[i]["getFullYear"]){return false}
						if (dojo.date.compare(a,b)){return false}	
					}else{
						if (!this._shallowCompare(a[i],b[i])){return false}
					}
				}else{	
					if (!b[i] || (a[i]!=b[i])){return false}
				}
			}

			//make sure there werent props on a that aren't on b, if there aren't, then
			//the previous section will have already evaluated things.

			for (var i in a){
				if (!b[i]){return false}
			}
			
			return true;
		},

		isItem: function(item){
			// summary:
			//	Checks to see if a passed 'item'
			//	is really a jsonPathStore item.  Currently
			//	it only verifies structure.  It does not verify
			//	that it belongs to this store at this time.
			//
			//	item: /* object */
			//	attribute: /* string */
		
			if (!dojo.isObject(item) || !item[this.metaLabel]){return false}
			if (this.requireId && this._hasId && !item[this._id]){return false}
			return true;
		},

		isItemLoaded: function(item){
			// summary:
			//	returns isItem() :)
			//
			//	item: /* object */

			item = this._correctReference(item);
			return this.isItem(item);
		},

		loadItem: function(item){
			// summary:
			//	returns true. Future implementatins might alter this 
			return true;
		},

		_updateMeta: function(item, props){
			// summary:
			//	verifies that 'item' has a meta object attached
			//	and if not it creates it by setting it to 'props'
			//	if the meta attribute already exists, mix 'props'
			//	into it.

			if (item && item[this.metaLabel]){
				dojo.mixin(item[this.metaLabel], props);
				return;
			}

			item[this.metaLabel]=props;
		},

		cleanMeta: function(data, options){
			// summary
			//	Recurses through 'data' and removes an
			//	meta information that has been attached. This
			//	function will also removes any id's that were autogenerated
			//	from objects.  It will not touch id's that were not generated

			data = data || this._data;

			if (data[this.metaLabel]){
				if(data[this.metaLabel]["autoId"]){
					delete data[this.idAttribute];
				}
				delete data[this.metaLabel];
			}

			if (dojo.isArray(data)){
				for(var i=0; i<data.length;i++){
					if(dojo.isObject(data[i]) || dojo.isArray(data[i]) ){
						this.cleanMeta(data[i]);
					}
				}
			} else if (dojo.isObject(data)){
				for (var i in data){
					this.cleanMeta(data[i]);
				}
			}
		}, 

		fetch: function(args){
			//console.log("fetch() ", args);
			// summary
			//	
			//	fetch takes either a string argument or a keywordArgs
			//	object containing the parameters for the search.
			//	If passed a string, fetch will interpret this string
			//	as the query to be performed and will do so in 
			//	SYNC_MODE returning the results immediately.
			//	If an object is supplied as 'args', its options will be 
			// 	parsed and then contained query executed. 
			//
			//	query: /* string or object */
			//		Defaults to "$..*". jsonPath query to be performed 
			//		on data store. **note that since some widgets
			//		expect this to be an object, an object in the form
			//		of {query: '$[*'], queryOptions: "someOptions"} is
			//		acceptable	
			//
			//	mode: dojox.data.SYNC_MODE || dojox.data.ASYNC_MODE
			//		Override the stores default mode.
			//
			//	queryOptions: /* object */
			//		Options passed on to the underlying jsonPath query
			//		system.
			//
			//	start: /* int */
			//		Starting item in result set
			//
			//	count: /* int */
			//		Maximum number of items to return
			//
			//	sort: /* function */
			//		Not Implemented yet
			//
			//	The following only apply to ASYNC requests (the default)
			//
			//	onBegin: /* function */
			//		called before any results are returned. Parameters
			//		will be the count and the original fetch request
			//	
			//	onItem: /*function*/
			//		called for each returned item.  Parameters will be
			//		the item and the fetch request
			//
			//	onComplete: /* function */
			//		called on completion of the request.  Parameters will	
			//		be the complete result set and the request
			//
			//	onError: /* function */
			//		colled in the event of an error

			// we're not started yet, add this request to a queue and wait till we do	
			if (!this._data){
				this._fetchQueue.push(args);
				return args;
			}	
			if(dojo.isString(args)){
					query = args;
					args={query: query, mode: dojox.data.SYNC_MODE};
					
			}

			var query;
			if (!args || !args.query){
				if (!args){
					var args={};	
				}

				if (!args.query){
					args.query="$..*";
					query=args.query;
				}

			}

			if (dojo.isObject(args.query)){
				if (args.query.query){
					query = args.query.query;
				}else{
					query = args.query = "$..*";
				}
				if (args.query.queryOptions){
					args.queryOptions=args.query.queryOptions
				}
			}else{
				query=args.query;
			}

			if (!args.mode) {args.mode = this.mode;}
			if (!args.queryOptions) {args.queryOptions={};}

			args.queryOptions.resultType='BOTH';
			var results = dojox.jsonPath.query(this._data, query, args.queryOptions);
			var tmp=[];
			var count=0;
			for (var i=0; i<results.length; i++){
				if(args.start && i<args.start){continue;}
				if (args.count && (count >= args.count)) { continue; }

				var item = results[i]["value"];
				var path = results[i]["path"];
				if (!dojo.isObject(item)){continue;}
				if(this.metaRegex.exec(path)){continue;}

				//this automatically records the objects path
				this._updateMeta(item,{path: results[i].path});

				//if autoIdentity and no id, generate one and add it to the item
				if(this.autoIdentity && !item[this.idAttribute]){
					var newId = this.autoIdPrefix + this._autoId++;
					item[this.idAttribute]=newId;
					item[this.metaLabel]["autoId"]=true;
				}

				//add item to the item index if appropriate
				if(item[this.idAttribute]){this.index[item[this.idAttribute]]=item}
				count++;
				tmp.push(item);
			}
			results = tmp;
			var scope = args.scope || dojo.global;

			if ("sort" in args){
				console.log("TODO::add support for sorting in the fetch");
			}	

			if (args.mode==dojox.data.SYNC_MODE){ 
				return results; 
			};

			if (args.onBegin){	
				args["onBegin"].call(scope, results.length, args);
			}

			if (args.onItem){
				for (var i=0; i<results.length;i++){	
					args["onItem"].call(scope, results[i], args);
				}
			}
 
			if (args.onComplete){
				args["onComplete"].call(scope, results, args);
			}

			return args;
		},

		dump: function(options){
			// summary:
			//
			//	exports the store data set. Takes an options
			//	object with a number of parameters
			//
			//	data: /* object */
			//		Defaults to the root of the store.
		 	//		The data to be exported.
			//	
			//	clone: /* boolean */
			//		clone the data set before returning it 
			//		or modifying it for export
			//
			//	cleanMeta: /* boolean */
			//		clean the meta data off of the data. Note
			//		that this will happen to the actual
			//		store data if !clone. If you want
			//		to continue using the store after
			//		this operation, it is probably better to export
			//		it as a clone if you want it cleaned.
			//
			//	suppressExportMeta: /* boolean */
			//		By default, when data is exported from the store
			//		some information, such as as a timestamp, is
			//		added to the root of exported data.  This
			//		prevents that from happening.  It is mainly used
			//		for making tests easier.
			//
			//	type: "raw" || "json"
			//		Defaults to 'json'. 'json' will convert the data into 
			//		json before returning it. 'raw' will just return a
			//		reference to the object	 

			var options = options || {};
			var d = options.data || this._data;
	
			if (!options.suppressExportMeta && options.clone){
				data = dojo.clone(d);
				if (data[this.metaLabel]){
					data[this.metaLabel]["clone"]=true;
				}
			}else{
				var data=d;
			}

			if (!options.suppressExportMeta &&  data[this.metaLabel]){
				data[this.metaLabel]["last_export"]=new Date().toString()
			}

			if(options.cleanMeta){
				this.cleanMeta(data);
			}

			//console.log("Exporting: ", options, dojo.toJson(data));	
			switch(options.type){
				case "raw":
					return data;
				case "json":
				default:
					return dojo.toJson(data, options.pretty || false);
			}
		},	

		getFeatures: function(){
			// summary:
			// 	return the store feature set

			return { 
				"dojo.data.api.Read": true,
				"dojo.data.api.Identity": true,
				"dojo.data.api.Write": true,
				"dojo.data.api.Notification": true
			}
		},

		getLabel: function(item){
			// summary
			//	returns the label for an item. The label
			//	is created by setting the store's labelAttribute 
			//	property with either an attribute name	or an array
			//	of attribute names.  Developers can also
			//	provide the store with a createLabel function which
			//	will do the actaul work of creating the label.  If not
			//	the default will just concatenate any of the identified
			//	attributes together.
			item = this._correctReference(item);
			var label="";

			if (dojo.isFunction(this.createLabel)){
				return this.createLabel(item);
			}

			if (this.labelAttribute){
				if (dojo.isArray(this.labelAttribute))	{
					for(var i=0; i<this.labelAttribute.length; i++){
						if (i>0) { label+=" ";}
						label += item[this.labelAttribute[i]];
					}
					return label;
				}else{
					return item[this.labelAttribute];
				}
			}
			return item.toString();
		},

		getLabelAttributes: function(item){
			// summary:
			//	returns an array of attributes that are used to create the label of an item
			item = this._correctReference(item);
			return dojo.isArray(this.labelAttribute) ? this.labelAttribute : [this.labelAttribute];
		},

		sort: function(a,b){
			console.log("TODO::implement default sort algo");
		},

		//Identity API Support

		getIdentity: function(item){
			// summary
			//	returns the identity of an item or throws
			//	a not found error.

			if (this.isItem(item)){
				return item[this.idAttribute];
			}
			throw new Error("Id not found for item");
		},

		getIdentityAttributes: function(item){
			// summary:
			//	returns the attributes which are used to make up the 
			//	identity of an item.  Basically returns this.idAttribute

			return [this.idAttribute];
		},

		fetchItemByIdentity: function(args){
			// summary: 
			//	fetch an item by its identity. This store also provides
			//	a much more finger friendly alias, 'byId' which does the
			//	same thing as this function.  If provided a string
			//	this call will be treated as a SYNC request and will 
			//	return the identified item immediatly.  Alternatively it
			// 	takes a object as a set of keywordArgs:
			//	
			//	identity: /* string */
			//		the id of the item you want to retrieve
			//	
			//	mode: dojox.data.SYNC_MODE || dojox.data.ASYNC_MODE
			//		overrides the default store fetch mode
			//	
			//	onItem: /* function */
			//		Result call back.  Passed the fetched item.
			//
			//	onError: /* function */
			//		error callback.	
			var id;	
			if (dojo.isString(args)){
				id = args;
				args = {identity: id, mode: dojox.data.SYNC_MODE}
			}else{
				if (args){
					id = args["identity"];		
				}
				if (!args.mode){args.mode = this.mode}	
			}

			if (this.index && (this.index[id] || this.index["identity"])){
				
				if (args.mode==dojox.data.SYNC_MODE){
					return this.index[id];
				}

				if (args.onItem){
					args["onItem"].call(args.scope || dojo.global, this.index[id], args);
				}

				return args;
			}else{
				if (args.mode==dojox.data.SYNC_MODE){
					return false;
				}
			}


			if(args.onError){
				args["onItem"].call(args.scope || dojo.global, new Error("Item Not Found: " + id), args);
			}
			
			return args;
		},

		//Write API Support
		newItem: function(data, options){
			// summary:
			//	adds a new item to the store at the specified point.
			//	Takes two parameters, data, and options. 
			//
			//	data: /* object */
			//		The data to be added in as an item.  This could be a
			//		new javascript object, or it could be an item that 
			//		already exists in the store.  If it already exists in the 
			//		store, then this will be added as a reference.  
			//
			//	options: /* object */
			//
			//		item: /* item */
			//			reference to an existing store item
			//
			//		attribute: /* string */
			//			attribute to add the item at.  If this is
			//			not provided, the item's id will be used as the
			//			attribute name. If specified attribute is an
			//			array, the new item will be push()d on to the
			//			end of it.
			//		oldValue: /* old value of item[attribute]
			//		newValue: new value item[attribute]

			var meta={};

			//default parent to the store root;
			var pInfo ={item:this._data};

			if (options){
				if (options.parent){
					options.item = options.parent;
				}

				dojo.mixin(pInfo, options);
			}

			if (this.idAttribute && !data[this.idAttribute]){
				if (this.requireId){throw new Error("requireId is enabled, new items must have an id defined to be added");}
				if (this.autoIdentity){
					var newId = this.autoIdPrefix + this._autoId++;
					data[this.idAttribute]=newId;
					meta["autoId"]=true;
				}
			}	

			if (!pInfo && !pInfo.attribute && !this.idAttribute && !data[this.idAttribute]){
				throw new Error("Adding a new item requires, at a minumum, either the pInfo information, including the pInfo.attribute, or an id on the item in the field identified by idAttribute");
			}

			//pInfo.parent = this._correctReference(pInfo.parent);
			//if there is no parent info supplied, default to the store root
			//and add to the pInfo.attribute or if that doestn' exist create an
			//attribute with the same name as the new items ID 
			if(!pInfo.attribute){pInfo.attribute = data[this.idAttribute]}

			pInfo.oldValue = this._trimItem(pInfo.item[pInfo.attribute]);
			if (dojo.isArray(pInfo.item[pInfo.attribute])){
				this._setDirty(pInfo.item);
				pInfo.item[pInfo.attribute].push(data);
			}else{
				this._setDirty(pInfo.item);
				pInfo.item[pInfo.attribute]=data;
			}

			pInfo.newValue = pInfo.item[pInfo.attribute];

			//add this item to the index
			if(data[this.idAttribute]){this.index[data[this.idAttribute]]=data}

			this._updateMeta(data, meta)

			//keep track of all references in the store so we can delete them as necessary
			this._addReference(data, pInfo);

			//mark this new item as dirty
			this._setDirty(data);

			//Notification API
			this.onNew(data, pInfo);

			//returns the original item, now decorated with some meta info
			return data;
		},

		_addReference: function(item, pInfo){
			// summary
			//	adds meta information to an item containing a reference id
			//	so that references can be deleted as necessary, when passed
			//	only a string, the string for parent info, it will only
			//	it will be treated as a string reference

			//console.log("_addReference: ", item, pInfo);	
			var rid = '_ref_' + this._referenceId++;
			if (!item[this.metaLabel]["referenceIds"]){
				item[this.metaLabel]["referenceIds"]=[];
			}

			item[this.metaLabel]["referenceIds"].push(rid);
			this._references[rid] = pInfo;				
		},

		deleteItem: function(item){	
			// summary
			//	deletes item and any references to that item from the store.
			//	If the desire is to delete only one reference, unsetAttribute or
			//	setValue is the way to go.

			item = this._correctReference(item);
			console.log("Item: ", item);
			if (this.isItem(item)){
				while(item[this.metaLabel]["referenceIds"].length>0){
					console.log("refs map: " , this._references);
					console.log("item to delete: ", item);
					var rid = item[this.metaLabel]["referenceIds"].pop();
					var pInfo = this._references[rid];

					console.log("deleteItem(): ", pInfo, pInfo.parent);
					var parentItem = pInfo.parent;
					var attribute = pInfo.attribute;	
					if(parentItem && parentItem[attribute] && !dojo.isArray(parentItem[attribute])){
						this._setDirty(parentItem);
						this.unsetAttribute(parentItem, attribute);
						delete parentItem[attribute];
					}

					if (dojo.isArray(parentItem[attribute])){
						console.log("Parent is array");
						var oldValue = this._trimItem(parentItem[attribute]);
						var found=false;
						for (var i=0; i<parentItem[attribute].length && !found;i++){
							if (parentItem[attribute][i][this.metaLabel]===item[this.metaLabel]){
								found=true;	
							}			
						}	

						if (found){
							this._setDirty(parentItem);
							var del =  parentItem[attribute].splice(i-1,1);
							delete del;
						}

						var newValue = this._trimItem(parentItem[attribute]);
						this.onSet(parentItem,attribute,oldValue,newValue);	
					}
					delete this._references[rid];

				}
				this.onDelete(item);		
				delete item;
			}
		},

		_setDirty: function(item){
			// summary:
			//	adds an item to the list of dirty items.  This item
			//	contains a reference to the item itself as well as a
			//	cloned and trimmed version of old item for use with
			//	revert.

			//if an item is already in the list of dirty items, don't add it again
			//or it will overwrite the premodification data set.
			for (var i=0; i<this._dirtyItems.length; i++){
				if (item[this.idAttribute]==this._dirtyItems[i][this.idAttribute]){
					return; 
				}	
			}

			this._dirtyItems.push({item: item, old: this._trimItem(item)});
			this._updateMeta(item, {isDirty: true});
		},

		setValue: function(item, attribute, value){
			// summary:
			//	sets 'attribute' on 'item' to 'value'
			item = this._correctReference(item);

			this._setDirty(item);
			var old = item[attribute] | undefined;
			item[attribute]=value;
			this.onSet(item,attribute,old,value);

		},

		setValues: function(item, attribute, values){
			// summary:
			//	sets 'attribute' on 'item' to 'value' value
			//	must be an array.


			item = this._correctReference(item);
			if (!dojo.isArray(values)){throw new Error("setValues expects to be passed an Array object as its value");}
			this._setDirty(item);
			var old = item[attribute] || null;
			item[attribute]=values
			this.onSet(item,attribute,old,values);
		},

		unsetAttribute: function(item, attribute){
			// summary:
			//	unsets 'attribute' on 'item'

			item = this._correctReference(item);
			this._setDirty(item);
			var old = item[attribute];
			delete item[attribute];
			this.onSet(item,attribute,old,null);
		},

		save: function(kwArgs){
			// summary:
			//	Takes an optional set of keyword Args with
			//	some save options.  Currently only format with options
			//	being "raw" or "json".  This function goes through
			//	the dirty item lists, clones and trims the item down so that
			//	the items children are not part of the data (the children are replaced
			//	with reference objects). This data is compiled into a single array, the dirty objects
			//	are all marked as clean, and the new data is then passed on to the onSave handler.

			var data = [];
		
			if (!kwArgs){kwArgs={}}
			while (this._dirtyItems.length > 0){
				var item = this._dirtyItems.pop()["item"];
				var t = this._trimItem(item);
				var d;	
				switch(kwArgs.format){	
					case "json":
						d = dojo.toJson(t);	
						break;
					case "raw":
					default:
						d = t;
				}
				data.push(d);
				this._markClean(item);
			}

			this.onSave(data);
		},

		_markClean: function(item){
			// summary
			//	remove this meta information marking an item as "dirty"

			if (item && item[this.metaLabel] && item[this.metaLabel]["isDirty"]){
				delete item[this.metaLabel]["isDirty"];
			}	
		},

		revert: function(){
			// summary
			//	returns any modified data to its original state prior to a save();

			while (this._dirtyItems.length>0){
				var d = this._dirtyItems.pop();
				this._mixin(d.item, d.old);
			}
			this.onRevert();
		},

		_mixin: function(target, data){
			// summary:
			//	specialized mixin that hooks up objects in the store where references are identified.

			if (dojo.isObject(data)){
				if (dojo.isArray(data)){
					while(target.length>0){target.pop();}
					for (var i=0; i<data.length;i++){
						if (dojo.isObject(data[i])){
							if (dojo.isArray(data[i])){
								var mix=[];
							}else{
								var mix={};
								if (data[i][this.metaLabel] && data[i][this.metaLabel]["type"] && data[i][this.metaLabel]["type"]=='reference'){
									target[i]=this.index[data[i][this.idAttribute]];
									continue;
								}
							}

							this._mixin(mix, data[i]);
							target.push(mix);
						}else{
							target.push(data[i]);
						}
					}	
				}else{
					for (var i in target){
						if (i in data){continue;}
						delete target[i];
					}

					for (var i in data){
						if (dojo.isObject(data[i])){
							if (dojo.isArray(data[i])){
								var mix=[];
							}else{
								if (data[i][this.metaLabel] && data[i][this.metaLabel]["type"] && data[i][this.metaLabel]["type"]=='reference'){
									target[i]=this.index[data[i][this.idAttribute]];
									continue;
								}

								var mix={};
							}
							this._mixin(mix, data[i]);
							target[i]=mix;
						}else{
							target[i]=data[i];
						}
					}	

				}
			}
		},

		isDirty: function(item){
			// summary
			//	returns true if the item is marked as dirty.

			item = this._correctReference(item);
			return item && item[this.metaLabel] && item[this.metaLabel]["isDirty"];
		},

		_createReference: function(item){
			// summary
			// 	Create a small reference object that can be used to replace
			//	child objects during a trim

			var obj={};
			obj[this.metaLabel]={
				type:'reference'
			};

			obj[this.idAttribute]=item[this.idAttribute];
			return obj;
		},

		_trimItem: function(item){
			//summary:
			// 	copy an item recursively stoppying at other items that have id's
			//	and replace them with a refrence object;
			var copy;
			if (dojo.isArray(item)){
				copy = [];
				for (var i=0; i<item.length;i++){
					if (dojo.isArray(item[i])){
						copy.push(this._trimItem(item[i]))
					}else if (dojo.isObject(item[i])){
						if (item[i]["getFullYear"]){
							copy.push(dojo.date.stamp.toISOString(item[i]));
						}else if (item[i][this.idAttribute]){
							copy.push(this._createReference(item[i]));
						}else{
							copy.push(this._trimItem(item[i]));	
						}
					} else {
						copy.push(item[i]);	
					}
				}
				return copy;
			} 

			if (dojo.isObject(item)){
				copy = {};

				for (var attr in item){
					if (!item[attr]){ copy[attr]=undefined;continue;}
					if (dojo.isArray(item[attr])){
						copy[attr] = this._trimItem(item[attr]);
					}else if (dojo.isObject(item[attr])){
						if (item[attr]["getFullYear"]){
							copy[attr] =  dojo.date.stamp.toISOString(item[attr]);
						}else if(item[attr][this.idAttribute]){
							copy[attr]=this._createReference(item[attr]);
						} else {
							copy[attr]=this._trimItem(item[attr]);
						}
					} else {
						copy[attr]=item[attr];
					}
				}
				return copy;
			}
		},

		//Notifcation Support

		onSet: function(){
		},

		onNew: function(){

		},

		onDelete: function(){

		},	
	
		onSave: function(items){
			// summary:
			//	notification of the save event..not part of the notification api, 
			//	but probably should be.
			//console.log("onSave() ", items);
		},

		onRevert: function(){
			// summary:
			//	notification of the revert event..not part of the notification api, 
			//	but probably should be.

		}
	}
);

//setup an alias to byId, is there a better way to do this?
dojox.data.jsonPathStore.byId=dojox.data.jsonPathStore.fetchItemByIdentity;