1 | define(["dojo/_base/kernel", "dojox", "dojo/date/stamp", "dojo/_base/array", "dojo/_base/json"], function(dojo, dojox){ |
---|
2 | |
---|
3 | dojo.getObject("json", true, dojox); |
---|
4 | |
---|
5 | return dojox.json.ref = { |
---|
6 | // summary: |
---|
7 | // Adds advanced JSON {de}serialization capabilities to the base json library. |
---|
8 | // This enhances the capabilities of dojo.toJson and dojo.fromJson, |
---|
9 | // adding referencing support, date handling, and other extra format handling. |
---|
10 | // On parsing, references are resolved. When references are made to |
---|
11 | // ids/objects that have been loaded yet, the loader function will be set to |
---|
12 | // _loadObject to denote a lazy loading (not loaded yet) object. |
---|
13 | |
---|
14 | |
---|
15 | resolveJson: function(/*Object*/ root,/*Object?*/ args){ |
---|
16 | // summary: |
---|
17 | // Indexes and resolves references in the JSON object. |
---|
18 | // description: |
---|
19 | // A JSON Schema object that can be used to advise the handling of the JSON (defining ids, date properties, urls, etc) |
---|
20 | // |
---|
21 | // root: |
---|
22 | // The root object of the object graph to be processed |
---|
23 | // args: |
---|
24 | // Object with additional arguments: |
---|
25 | // |
---|
26 | // The *index* parameter. |
---|
27 | // This is the index object (map) to use to store an index of all the objects. |
---|
28 | // If you are using inter-message referencing, you must provide the same object for each call. |
---|
29 | // The *defaultId* parameter. |
---|
30 | // This is the default id to use for the root object (if it doesn't define it's own id) |
---|
31 | // The *idPrefix* parameter. |
---|
32 | // This the prefix to use for the ids as they enter the index. This allows multiple tables |
---|
33 | // to use ids (that might otherwise collide) that enter the same global index. |
---|
34 | // idPrefix should be in the form "/Service/". For example, |
---|
35 | // if the idPrefix is "/Table/", and object is encountered {id:"4",...}, this would go in the |
---|
36 | // index as "/Table/4". |
---|
37 | // The *idAttribute* parameter. |
---|
38 | // This indicates what property is the identity property. This defaults to "id" |
---|
39 | // The *assignAbsoluteIds* parameter. |
---|
40 | // This indicates that the resolveJson should assign absolute ids (__id) as the objects are being parsed. |
---|
41 | // |
---|
42 | // The *schemas* parameter |
---|
43 | // This provides a map of schemas, from which prototypes can be retrieved |
---|
44 | // The *loader* parameter |
---|
45 | // This is a function that is called added to the reference objects that can't be resolved (lazy objects) |
---|
46 | // return: |
---|
47 | // An object, the result of the processing |
---|
48 | args = args || {}; |
---|
49 | var idAttribute = args.idAttribute || 'id'; |
---|
50 | var refAttribute = this.refAttribute; |
---|
51 | var idAsRef = args.idAsRef; |
---|
52 | var prefix = args.idPrefix || ''; |
---|
53 | var assignAbsoluteIds = args.assignAbsoluteIds; |
---|
54 | var index = args.index || {}; // create an index if one doesn't exist |
---|
55 | var timeStamps = args.timeStamps; |
---|
56 | var ref,reWalk=[]; |
---|
57 | var pathResolveRegex = /^(.*\/)?(\w+:\/\/)|[^\/\.]+\/\.\.\/|^.*\/(\/)/; |
---|
58 | var addProp = this._addProp; |
---|
59 | var F = function(){}; |
---|
60 | function walk(it, stop, defaultId, needsPrefix, schema, defaultObject){ |
---|
61 | // this walks the new graph, resolving references and making other changes |
---|
62 | var i, update, val, id = idAttribute in it ? it[idAttribute] : defaultId; |
---|
63 | if(idAttribute in it || ((id !== undefined) && needsPrefix)){ |
---|
64 | id = (prefix + id).replace(pathResolveRegex,'$2$3'); |
---|
65 | } |
---|
66 | var target = defaultObject || it; |
---|
67 | if(id !== undefined){ // if there is an id available... |
---|
68 | if(assignAbsoluteIds){ |
---|
69 | it.__id = id; |
---|
70 | } |
---|
71 | if(args.schemas && (!(it instanceof Array)) && // won't try on arrays to do prototypes, plus it messes with queries |
---|
72 | (val = id.match(/^(.+\/)[^\.\[]*$/))){ // if it has a direct table id (no paths) |
---|
73 | schema = args.schemas[val[1]]; |
---|
74 | } |
---|
75 | // if the id already exists in the system, we should use the existing object, and just |
---|
76 | // update it... as long as the object is compatible |
---|
77 | if(index[id] && ((it instanceof Array) == (index[id] instanceof Array))){ |
---|
78 | target = index[id]; |
---|
79 | delete target.$ref; // remove this artifact |
---|
80 | delete target._loadObject; |
---|
81 | update = true; |
---|
82 | }else{ |
---|
83 | var proto = schema && schema.prototype; // and if has a prototype |
---|
84 | if(proto){ |
---|
85 | // if the schema defines a prototype, that needs to be the prototype of the object |
---|
86 | F.prototype = proto; |
---|
87 | target = new F(); |
---|
88 | } |
---|
89 | } |
---|
90 | index[id] = target; // add the prefix, set _id, and index it |
---|
91 | if(timeStamps){ |
---|
92 | timeStamps[id] = args.time; |
---|
93 | } |
---|
94 | } |
---|
95 | while(schema){ |
---|
96 | var properties = schema.properties; |
---|
97 | if(properties){ |
---|
98 | for(i in it){ |
---|
99 | var propertyDefinition = properties[i]; |
---|
100 | if(propertyDefinition && propertyDefinition.format == 'date-time' && typeof it[i] == 'string'){ |
---|
101 | it[i] = dojo.date.stamp.fromISOString(it[i]); |
---|
102 | } |
---|
103 | } |
---|
104 | } |
---|
105 | schema = schema["extends"]; |
---|
106 | } |
---|
107 | var length = it.length; |
---|
108 | for(i in it){ |
---|
109 | if(i==length){ |
---|
110 | break; |
---|
111 | } |
---|
112 | if(it.hasOwnProperty(i)){ |
---|
113 | val=it[i]; |
---|
114 | if((typeof val =='object') && val && !(val instanceof Date) && i != '__parent'){ |
---|
115 | ref=val[refAttribute] || (idAsRef && val[idAttribute]); |
---|
116 | if(!ref || !val.__parent){ |
---|
117 | if(it != reWalk){ |
---|
118 | val.__parent = target; |
---|
119 | } |
---|
120 | } |
---|
121 | if(ref){ // a reference was found |
---|
122 | // make sure it is a safe reference |
---|
123 | delete it[i];// remove the property so it doesn't resolve to itself in the case of id.propertyName lazy values |
---|
124 | var path = ref.toString().replace(/(#)([^\.\[])/,'$1.$2').match(/(^([^\[]*\/)?[^#\.\[]*)#?([\.\[].*)?/); // divide along the path |
---|
125 | if(index[(prefix + ref).replace(pathResolveRegex,'$2$3')]){ |
---|
126 | ref = index[(prefix + ref).replace(pathResolveRegex,'$2$3')]; |
---|
127 | }else if((ref = (path[1]=='$' || path[1]=='this' || path[1]=='') ? root : index[(prefix + path[1]).replace(pathResolveRegex,'$2$3')])){ // a $ indicates to start with the root, otherwise start with an id |
---|
128 | // if there is a path, we will iterate through the path references |
---|
129 | if(path[3]){ |
---|
130 | path[3].replace(/(\[([^\]]+)\])|(\.?([^\.\[]+))/g,function(t,a,b,c,d){ |
---|
131 | ref = ref && ref[b ? b.replace(/[\"\'\\]/,'') : d]; |
---|
132 | }); |
---|
133 | } |
---|
134 | } |
---|
135 | if(ref){ |
---|
136 | val = ref; |
---|
137 | }else{ |
---|
138 | // otherwise, no starting point was found (id not found), if stop is set, it does not exist, we have |
---|
139 | // unloaded reference, if stop is not set, it may be in a part of the graph not walked yet, |
---|
140 | // we will wait for the second loop |
---|
141 | if(!stop){ |
---|
142 | var rewalking; |
---|
143 | if(!rewalking){ |
---|
144 | reWalk.push(target); // we need to rewalk it to resolve references |
---|
145 | } |
---|
146 | rewalking = true; // we only want to add it once |
---|
147 | val = walk(val, false, val[refAttribute], true, propertyDefinition); |
---|
148 | // create a lazy loaded object |
---|
149 | val._loadObject = args.loader; |
---|
150 | } |
---|
151 | } |
---|
152 | }else{ |
---|
153 | if(!stop){ // if we are in stop, that means we are in the second loop, and we only need to check this current one, |
---|
154 | // further walking may lead down circular loops |
---|
155 | val = walk( |
---|
156 | val, |
---|
157 | reWalk==it, |
---|
158 | id === undefined ? undefined : addProp(id, i), // the default id to use |
---|
159 | false, |
---|
160 | propertyDefinition, |
---|
161 | // if we have an existing object child, we want to |
---|
162 | // maintain it's identity, so we pass it as the default object |
---|
163 | target != it && typeof target[i] == 'object' && target[i] |
---|
164 | ); |
---|
165 | } |
---|
166 | } |
---|
167 | } |
---|
168 | it[i] = val; |
---|
169 | if(target!=it && !target.__isDirty){// do updates if we are updating an existing object and it's not dirty |
---|
170 | var old = target[i]; |
---|
171 | target[i] = val; // only update if it changed |
---|
172 | if(update && val !== old && // see if it is different |
---|
173 | !target._loadObject && // no updates if we are just lazy loading |
---|
174 | !(i.charAt(0) == '_' && i.charAt(1) == '_') && i != "$ref" && |
---|
175 | !(val instanceof Date && old instanceof Date && val.getTime() == old.getTime()) && // make sure it isn't an identical date |
---|
176 | !(typeof val == 'function' && typeof old == 'function' && val.toString() == old.toString()) && // make sure it isn't an indentical function |
---|
177 | index.onUpdate){ |
---|
178 | index.onUpdate(target,i,old,val); // call the listener for each update |
---|
179 | } |
---|
180 | } |
---|
181 | } |
---|
182 | } |
---|
183 | |
---|
184 | if(update && (idAttribute in it || target instanceof Array)){ |
---|
185 | // this means we are updating with a full representation of the object, we need to remove deleted |
---|
186 | for(i in target){ |
---|
187 | if(!target.__isDirty && target.hasOwnProperty(i) && !it.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && !(target instanceof Array && isNaN(i))){ |
---|
188 | if(index.onUpdate && i != "_loadObject" && i != "_idAttr"){ |
---|
189 | index.onUpdate(target,i,target[i],undefined); // call the listener for each update |
---|
190 | } |
---|
191 | delete target[i]; |
---|
192 | while(target instanceof Array && target.length && target[target.length-1] === undefined){ |
---|
193 | // shorten the target if necessary |
---|
194 | target.length--; |
---|
195 | } |
---|
196 | } |
---|
197 | } |
---|
198 | }else{ |
---|
199 | if(index.onLoad){ |
---|
200 | index.onLoad(target); |
---|
201 | } |
---|
202 | } |
---|
203 | return target; |
---|
204 | } |
---|
205 | if(root && typeof root == 'object'){ |
---|
206 | root = walk(root,false,args.defaultId, true); // do the main walk through |
---|
207 | walk(reWalk,false); // re walk any parts that were not able to resolve references on the first round |
---|
208 | } |
---|
209 | return root; |
---|
210 | }, |
---|
211 | |
---|
212 | |
---|
213 | fromJson: function(/*String*/ str,/*Object?*/ args){ |
---|
214 | // summary: |
---|
215 | // evaluates the passed string-form of a JSON object. |
---|
216 | // |
---|
217 | // str: |
---|
218 | // a string literal of a JSON item, for instance: |
---|
219 | // '{ "foo": [ "bar", 1, { "baz": "thud" } ] }' |
---|
220 | // args: See resolveJson |
---|
221 | // |
---|
222 | // return: |
---|
223 | // An object, the result of the evaluation |
---|
224 | function ref(target){ // support call styles references as well |
---|
225 | var refObject = {}; |
---|
226 | refObject[this.refAttribute] = target; |
---|
227 | return refObject; |
---|
228 | } |
---|
229 | try{ |
---|
230 | var root = eval('(' + str + ')'); // do the eval |
---|
231 | }catch(e){ |
---|
232 | throw new SyntaxError("Invalid JSON string: " + e.message + " parsing: "+ str); |
---|
233 | } |
---|
234 | if(root){ |
---|
235 | return this.resolveJson(root, args); |
---|
236 | } |
---|
237 | return root; |
---|
238 | }, |
---|
239 | |
---|
240 | toJson: function(/*Object*/ it, /*Boolean?*/ prettyPrint, /*Object?*/ idPrefix, /*Object?*/ indexSubObjects){ |
---|
241 | // summary: |
---|
242 | // Create a JSON serialization of an object. |
---|
243 | // This has support for referencing, including circular references, duplicate references, and out-of-message references |
---|
244 | // 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/. |
---|
245 | // |
---|
246 | // it: |
---|
247 | // an object to be serialized. |
---|
248 | // |
---|
249 | // prettyPrint: |
---|
250 | // if true, we indent objects and arrays to make the output prettier. |
---|
251 | // The variable dojo.toJsonIndentStr is used as the indent string |
---|
252 | // -- to use something other than the default (tab), |
---|
253 | // change that variable before calling dojo.toJson(). |
---|
254 | // |
---|
255 | // idPrefix: The prefix that has been used for the absolute ids |
---|
256 | // |
---|
257 | // return: |
---|
258 | // a String representing the serialized version of the passed object. |
---|
259 | var useRefs = this._useRefs; |
---|
260 | var addProp = this._addProp; |
---|
261 | var refAttribute = this.refAttribute; |
---|
262 | idPrefix = idPrefix || ''; // the id prefix for this context |
---|
263 | var paths={}; |
---|
264 | var generated = {}; |
---|
265 | function serialize(it,path,_indentStr){ |
---|
266 | if(typeof it == 'object' && it){ |
---|
267 | var value; |
---|
268 | if(it instanceof Date){ // properly serialize dates |
---|
269 | return '"' + dojo.date.stamp.toISOString(it,{zulu:true}) + '"'; |
---|
270 | } |
---|
271 | var id = it.__id; |
---|
272 | if(id){ // we found an identifiable object, we will just serialize a reference to it... unless it is the root |
---|
273 | if(path != '#' && ((useRefs && !id.match(/#/)) || paths[id])){ |
---|
274 | var ref = id; |
---|
275 | if(id.charAt(0)!='#'){ |
---|
276 | if(it.__clientId == id){ |
---|
277 | ref = "cid:" + id; |
---|
278 | }else if(id.substring(0, idPrefix.length) == idPrefix){ // see if the reference is in the current context |
---|
279 | // a reference with a prefix matching the current context, the prefix should be removed |
---|
280 | ref = id.substring(idPrefix.length); |
---|
281 | }else{ |
---|
282 | // a reference to a different context, assume relative url based referencing |
---|
283 | ref = id; |
---|
284 | } |
---|
285 | } |
---|
286 | var refObject = {}; |
---|
287 | refObject[refAttribute] = ref; |
---|
288 | return serialize(refObject,'#'); |
---|
289 | } |
---|
290 | path = id; |
---|
291 | }else{ |
---|
292 | it.__id = path; // we will create path ids for other objects in case they are circular |
---|
293 | generated[path] = it; |
---|
294 | } |
---|
295 | paths[path] = it;// save it here so they can be deleted at the end |
---|
296 | _indentStr = _indentStr || ""; |
---|
297 | var nextIndent = prettyPrint ? _indentStr + dojo.toJsonIndentStr : ""; |
---|
298 | var newLine = prettyPrint ? "\n" : ""; |
---|
299 | var sep = prettyPrint ? " " : ""; |
---|
300 | |
---|
301 | if(it instanceof Array){ |
---|
302 | var res = dojo.map(it, function(obj,i){ |
---|
303 | var val = serialize(obj, addProp(path, i), nextIndent); |
---|
304 | if(typeof val != "string"){ |
---|
305 | val = "undefined"; |
---|
306 | } |
---|
307 | return newLine + nextIndent + val; |
---|
308 | }); |
---|
309 | return "[" + res.join("," + sep) + newLine + _indentStr + "]"; |
---|
310 | } |
---|
311 | |
---|
312 | var output = []; |
---|
313 | for(var i in it){ |
---|
314 | if(it.hasOwnProperty(i)){ |
---|
315 | var keyStr; |
---|
316 | if(typeof i == "number"){ |
---|
317 | keyStr = '"' + i + '"'; |
---|
318 | }else if(typeof i == "string" && (i.charAt(0) != '_' || i.charAt(1) != '_')){ |
---|
319 | // we don't serialize our internal properties __id and __clientId |
---|
320 | keyStr = dojo._escapeString(i); |
---|
321 | }else{ |
---|
322 | // skip non-string or number keys |
---|
323 | continue; |
---|
324 | } |
---|
325 | var val = serialize(it[i],addProp(path, i),nextIndent); |
---|
326 | if(typeof val != "string"){ |
---|
327 | // skip non-serializable values |
---|
328 | continue; |
---|
329 | } |
---|
330 | output.push(newLine + nextIndent + keyStr + ":" + sep + val); |
---|
331 | } |
---|
332 | } |
---|
333 | return "{" + output.join("," + sep) + newLine + _indentStr + "}"; |
---|
334 | }else if(typeof it == "function" && dojox.json.ref.serializeFunctions){ |
---|
335 | return it.toString(); |
---|
336 | } |
---|
337 | |
---|
338 | return dojo.toJson(it); // use the default serializer for primitives |
---|
339 | } |
---|
340 | var json = serialize(it,'#',''); |
---|
341 | if(!indexSubObjects){ |
---|
342 | for(var i in generated) {// cleanup the temporary path-generated ids |
---|
343 | delete generated[i].__id; |
---|
344 | } |
---|
345 | } |
---|
346 | return json; |
---|
347 | }, |
---|
348 | _addProp: function(id, prop){ |
---|
349 | return id + (id.match(/#/) ? id.length == 1 ? '' : '.' : '#') + prop; |
---|
350 | }, |
---|
351 | // refAttribute: String |
---|
352 | // This indicates what property is the reference property. This acts like the idAttribute |
---|
353 | // except that this is used to indicate the current object is a reference or only partially |
---|
354 | // loaded. This defaults to "$ref". |
---|
355 | refAttribute: "$ref", |
---|
356 | _useRefs: false, |
---|
357 | serializeFunctions: false |
---|
358 | }; |
---|
359 | }); |
---|