aboutsummaryrefslogtreecommitdiff
path: root/includes/js/dojox/rpc/JsonReferencing.js
blob: 618ea0688db2c65b7ba00ab7e7052598266c53c9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
if(!dojo._hasResource["dojox.rpc.JsonReferencing"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.rpc.JsonReferencing"] = true;
dojo.provide("dojox.rpc.JsonReferencing");
dojo.require("dojo.date.stamp");
dojo.require("dojo._base.Deferred");

// summary:
// Adds advanced JSON {de}serialization capabilities to the base json library.
// This enhances the capabilities of dojo.toJson and dojo.fromJson,
// adding referencing support, date handling, and other extra format handling.
// On parsing, references are resolved. When references are made to 
// ids/objects that have been loaded yet, a Deferred object will be used as the
// value and as soon as a callback is added to the Deferred object, the target
// object will be loaded.
 


dojox.rpc._index={}; // the global map of id->object
dojox.rpc.onUpdate = function(/*Object*/ object,  /* attribute-name-string */ attribute,  /* any */ oldValue,  /* any */ newValue){
		//	summary:
		//		This function is called when an existing object in the system is updated. Existing objects are found by id. 
};

dojox.rpc.resolveJson = function(/*Object*/ root,/*Object?*/ schema){
	// summary:
	// 		Indexes and resolves references in the JSON object. 
	// A JSON Schema object that can be used to advise the handling of the JSON (defining ids, date properties, urls, etc)
	//  
	// root: 
	//		The root object of the object graph to be processed
	//
	// schema: A JSON Schema object that can be used to advise the parsing of the JSON (defining ids, date properties, urls, etc)	//
	// 		Currently this provides a means for context based id handling
	//
	// return:
	//		An object, the result of the processing
	var ref,reWalk=[];
	function makeIdInfo(schema){ // find out what attribute and what id prefix to use
		if (schema){
			var attr;
			if (!(attr = schema._idAttr)){
				for (var i in schema.properties){
					if (schema.properties[i].unique){
						schema._idAttr = attr = i;
					}
				}
			}
			if (attr || schema._idPrefix){
				return {attr:attr || 'id',prefix:schema._idPrefix};
			}
		}

		return false;
	}
	function walk(it,stop,schema,idInfo,defaultId){
		// this walks the new graph, resolving references and making other changes 
	 	var val,i;
	 	var id = it[idInfo.attr];
	 	id = (id && (idInfo.prefix + id)) || defaultId; // if there is an id, prefix it, otherwise inherit 
	 	var target = it;
	 	
		if (id){ // if there is an id available...
			it._id = id;
			if (dojox.rpc._index[id]){ // if the id already exists in the system, we should use the existing object, and just update it
				target = dojox.rpc._index[id];
				delete target.$ref; // remove this artifact
			}
			dojox.rpc._index[id] = target; // add the prefix, set _id, and index it
			if (schema && dojox.validate && dojox.validate.jsonSchema){ // if json schema is activated, we can load it in the registry of instance schemas map
				dojox.validate.jsonSchema._schemas[id] = schema;
			}
			
		}		
		for (i in it){
			if (it.hasOwnProperty(i) && (typeof (val=it[i]) =='object') && val){
				ref=val.$ref;
				if (ref){ // a reference was found
					var stripped = ref.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');// trim it
					if(/[\w\[\]\.\$ \/\r\n\t]/.test(stripped) && !/=|((^|\W)new\W)/.test(stripped)){ // make sure it is a safe reference
						var path = ref.match(/(^\.*[^\.\[]+)([\.\[].*)?/); // divide along the path
						if ((ref=path[1]=='$' ? root:dojox.rpc._index[new dojo._Url(idInfo.prefix,path[1])]) &&  // a $ indicates to start with the root, otherwise start with an id 
							(ref = path[2] ? eval('ref' + path[2]) : ref)){// starting point was found, use eval to resolve remaining property references
							// otherwise, no starting point was found (id not found), if stop is set, it does not exist, we have 
							// unloaded reference, if stop is not set, it may be in a part of the graph not walked yet, 
							// we will wait for the second loop
							val = ref;
						}
						else{
							if (!stop){
								if (!rewalking) 
									reWalk.push(it); // we need to rewalk it to resolve references
								var rewalking = true; // we only want to add it once
							}
							else {
								ref = val.$ref;
								val = new dojo.Deferred();
								val._id = idInfo.prefix + ref;
								(function(val,ref){
									var connectId = dojo.connect(val,"addCallbacks",function(){
										dojo.disconnect(connectId);
										dojox.rpc.services[idInfo.prefix.substring(0,idInfo.prefix.length-1)](ref) // otherwise call by looking up the service 
											.addCallback(dojo.hitch(val,val.callback));
										
									});
								})(val,ref);
							}
						}
					}
				}
				else {
					if (!stop){ // if we are in stop, that means we are in the second loop, and we only need to check this current one,
										// further walking may lead down circular loops
						var valSchema = val.schema || // a schema can be self-defined by the object,  
										(schema && schema.properties && schema.properties[i]);  // or it can from the schema sub-object definition 
						if (valSchema){
							idInfo = makeIdInfo(valSchema)||idInfo;
						}
						val = walk(val,reWalk==it,valSchema,idInfo,id && (id + ('[' + dojo._escapeString(i) + ']')));
					}
				}
			}
			if (dojo.isString(val) && schema && schema.properties && schema.properties[i] && schema.properties[i].format=='date-time'){// parse the date string
				val = dojo.date.stamp.fromISOString(val); // create a date object
			}
			it[i] = val;
			var old = target[i];  
			if (val !== old){ // only update if it changed
				target[i] = val; // update the target
				propertyChange(i,old,val);
			}
		}
		function propertyChange(key,old,newValue){
			setTimeout(function(){
				dojox.rpc.onUpdate(target,i,old,newValue); // call the listener for each update
			});
		}
		if (target != it){ // this means we are updating, we need to remove deleted
			for (i in target){
				if (!it.hasOwnProperty(i) && i != '_id' && !(target instanceof Array && isNaN(i))){
					propertyChange(i,target[i],undefined);
					delete target[i];
				}
			}
		}
		return target;
	}
	var idInfo = makeIdInfo(schema)||{attr:'id',prefix:''};
	if (!root){ return root; } 
	root = walk(root,false,schema,idInfo,dojox._newId && (new dojo._Url(idInfo.prefix,dojox._newId) +'')); // do the main walk through
	walk(reWalk,false,schema,idInfo); // re walk any parts that were not able to resolve references on the first round
	return root;
};
dojox.rpc.fromJson = function(/*String*/ str,/*Object?*/ schema){
	// summary:
	// 		evaluates the passed string-form of a JSON object. 
	// A JSON Schema object that can be used to advise the parsing of the JSON (defining ids, date properties, urls, etc)
	// which may defined by setting dojox.currentSchema to the current schema you want to use for this evaluation
	//  
	// json: 
	//		a string literal of a JSON item, for instance:
	//			'{ "foo": [ "bar", 1, { "baz": "thud" } ] }'
	// schema: A JSON Schema object that can be used to advise the parsing of the JSON (defining ids, date properties, urls, etc)	//
	// 		Currently this provides a means for context based id handling
	//
	// return:
	//		An object, the result of the evaluation
	root = eval('(' + str + ')'); // do the eval
	if (root){
		return this.resolveJson(root,schema);
	}
	return root;
}
dojox.rpc.toJson = function(/*Object*/ it, /*Boolean?*/ prettyPrint, /*Object?*/ schema){
	// summary:
	//		Create a JSON serialization of an object. 
	//		This has support for referencing, including circular references, duplicate references, and out-of-message references
	// 		id and path-based referencing is supported as well and is based on http://www.json.com/2007/10/19/json-referencing-proposal-and-library/.
	//
	// it:
	//		an object to be serialized. 
	//
	// prettyPrint:
	//		if true, we indent objects and arrays to make the output prettier.
	//		The variable dojo.toJsonIndentStr is used as the indent string 
	//		-- to use something other than the default (tab), 
	//		change that variable before calling dojo.toJson().
	//
	// schema: A JSON Schema object that can be used to advise the parsing of the JSON (defining ids, date properties, urls, etc)	//
	// 		Currently this provides a means for context based id handling
	// 		
	// return:
	//		a String representing the serialized version of the passed object.
	
	var idPrefix = (schema&& schema._idPrefix) || ''; // the id prefix for this context 
	var paths={};
	function serialize(it,path,_indentStr){ 
		if (it && dojo.isObject(it)){
			var value;
			if (it instanceof Date){ // properly serialize dates
				return '"' + dojo.date.stamp.toISOString(it,{zulu:true}) + '"';
			}
			var id = it._id;
			if (id){ // we found an identifiable object, we will just serialize a reference to it... unless it is the root
				
				if (path != '$'){
					return serialize({$ref:id.charAt(0)=='$' ? id : // a pure path based reference, leave it alone
 									id.substring(0,idPrefix.length)==idPrefix ?  // see if the reference is in the current context
 										id.substring(idPrefix.length): // a reference with a prefix matching the current context, the prefix should be removed
	 										'../' + id});// a reference to a different context, assume relative url based referencing
				}
				path = id;
			}
			else {
				it._id = path; // we will create path ids for other objects in case they are circular 
				paths[path] = it;// save it here so they can be deleted at the end
			}
			_indentStr = _indentStr || "";
			var nextIndent = prettyPrint ? _indentStr + dojo.toJsonIndentStr : "";
			var newLine = prettyPrint ? "\n" : "";
			var sep = prettyPrint ? " " : "";
			
			if (it instanceof Array){
				var res = dojo.map(it, function(obj,i){
					var val = serialize(obj, path + '[' + i + ']', nextIndent);
					if(!dojo.isString(val)){
						val = "undefined";
					}
					return newLine + nextIndent + val;
				});
				return "[" + res.join("," + sep) + newLine + _indentStr + "]";
			} 
			
			var output = [];
			for(var i in it){
				var keyStr;
				if(typeof i == "number"){
					keyStr = '"' + i + '"';
				}else if(dojo.isString(i) && i != '_id'){
					keyStr = dojo._escapeString(i);
				}else{
					// skip non-string or number keys
					continue;
				}
				var val = serialize(it[i],path+(i.match(/^[a-zA-Z]\w*$/) ? // can we use simple .property syntax? 
													('.' + i) : // yes, otherwise we have to escape it
													('[' + dojo._escapeString(i) + ']')),nextIndent);
				if(!dojo.isString(val)){
					// skip non-serializable values
					continue;
				}
				output.push(newLine + nextIndent + keyStr + ":" + sep + val);
			}
			return "{" + output.join("," + sep) + newLine + _indentStr + "}";
		}
		
		return dojo.toJson(it); // use the default serializer for primitives
	}
	var json = serialize(it,'$','');
	for (i in paths){  // cleanup the temporary path-generated ids
		delete paths[i]._id;
	}
	return json;
}

}