You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
383 lines
15 KiB
383 lines
15 KiB
define([ |
|
"dojo/_base/array", |
|
"dojo/_base/json", |
|
"dojo/_base/kernel", |
|
"dojo/_base/lang", |
|
"dojo/date/stamp", |
|
"dojox" |
|
], |
|
function(array, djson, dojo, lang, stamp, dojox){ |
|
|
|
lang.getObject("json", true, dojox); |
|
|
|
return dojox.json.ref = { |
|
// 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, the loader function will be set to |
|
// _loadObject to denote a lazy loading (not loaded yet) object. |
|
|
|
|
|
resolveJson: function(/*Object*/ root, /*Object?*/ args){ |
|
// summary: |
|
// Indexes and resolves references in the JSON object. |
|
// description: |
|
// 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 |
|
// args: |
|
// Object with additional arguments: |
|
// |
|
// - The *index* parameter: |
|
// This is the index object (map) to use to store an index of all the objects. |
|
// If you are using inter-message referencing, you must provide the same object for each call. |
|
// - The *defaultId* parameter: |
|
// This is the default id to use for the root object (if it doesn't define it's own id) |
|
// - The *idPrefix* parameter: |
|
// This the prefix to use for the ids as they enter the index. This allows multiple tables |
|
// to use ids (that might otherwise collide) that enter the same global index. |
|
// idPrefix should be in the form "/Service/". For example, |
|
// if the idPrefix is "/Table/", and object is encountered {id:"4",...}, this would go in the |
|
// index as "/Table/4". |
|
// - The *idAttribute* parameter: |
|
// This indicates what property is the identity property. This defaults to "id" |
|
// - The *assignAbsoluteIds* parameter: |
|
// This indicates that the resolveJson should assign absolute ids (__id) as the objects are being parsed. |
|
// - The *schemas* parameter: |
|
// This provides a map of schemas, from which prototypes can be retrieved |
|
// - The *loader* parameter: |
|
// This is a function that is called added to the reference objects that can't be resolved (lazy objects) |
|
// returns: |
|
// An object, the result of the processing |
|
args = args || {}; |
|
var idAttribute = args.idAttribute || 'id'; |
|
var refAttribute = this.refAttribute; |
|
var idAsRef = args.idAsRef; |
|
var prefix = args.idPrefix || ''; |
|
var assignAbsoluteIds = args.assignAbsoluteIds; |
|
var index = args.index || {}; // create an index if one doesn't exist |
|
var timeStamps = args.timeStamps; |
|
var ref,reWalk=[]; |
|
var pathResolveRegex = /^(.*\/)?(\w+:\/\/)|[^\/\.]+\/\.\.\/|^.*\/(\/)/; |
|
var addProp = this._addProp; |
|
var F = function(){}; |
|
function walk(it, stop, defaultId, needsPrefix, schema, defaultObject, parent){ |
|
// this walks the new graph, resolving references and making other changes |
|
var i, update, val, id = idAttribute in it ? it[idAttribute] : defaultId; |
|
if(idAttribute in it || ((id !== undefined) && needsPrefix)){ |
|
id = (prefix + id).replace(pathResolveRegex,'$2$3'); |
|
} |
|
var target = defaultObject || it; |
|
if(id !== undefined){ // if there is an id available... |
|
if(assignAbsoluteIds){ |
|
it.__id = id; |
|
} |
|
if(args.schemas && (!(it instanceof Array)) && // won't try on arrays to do prototypes, plus it messes with queries |
|
(val = id.match(/^(.+\/)[^\.\[]*$/))){ // if it has a direct table id (no paths) |
|
schema = args.schemas[val[1]]; |
|
} |
|
// if the id already exists in the system, we should use the existing object, and just |
|
// update it... as long as the object is compatible |
|
if(index[id] && ((it instanceof Array) == (index[id] instanceof Array))){ |
|
target = index[id]; |
|
delete target.$ref; // remove this artifact |
|
delete target._loadObject; |
|
update = true; |
|
}else{ |
|
var proto = schema && schema.prototype; // and if has a prototype |
|
if(proto){ |
|
// if the schema defines a prototype, that needs to be the prototype of the object |
|
F.prototype = proto; |
|
target = new F(); |
|
} |
|
} |
|
index[id] = target; // add the prefix, set _id, and index it |
|
if(timeStamps){ |
|
timeStamps[id] = args.time; |
|
} |
|
} |
|
while(schema){ |
|
var properties = schema.properties; |
|
if(properties){ |
|
for(i in it){ |
|
var propertyDefinition = properties[i]; |
|
if(propertyDefinition && propertyDefinition.format == 'date-time' && typeof it[i] == 'string'){ |
|
it[i] = stamp.fromISOString(it[i]); |
|
} |
|
} |
|
} |
|
schema = schema["extends"]; |
|
} |
|
var length = it.length; |
|
for(i in it){ |
|
if(i==length){ |
|
break; |
|
} |
|
if(it.hasOwnProperty(i)){ |
|
val=it[i]; |
|
if((typeof val =='object') && val && !(val instanceof Date) && i != '__parent'){ |
|
ref=val[refAttribute] || (idAsRef && val[idAttribute]); |
|
if(it != reWalk && (!ref || !val.__parent)){ |
|
val.__parent = parent ? parent : target; |
|
} |
|
if(ref){ // a reference was found |
|
// make sure it is a safe reference |
|
delete it[i];// remove the property so it doesn't resolve to itself in the case of id.propertyName lazy values |
|
var path = ref.toString().replace(/(#)([^\.\[])/,'$1.$2').match(/(^([^\[]*\/)?[^#\.\[]*)#?([\.\[].*)?/); // divide along the path |
|
if(index[(prefix + ref).replace(pathResolveRegex,'$2$3')]){ |
|
ref = index[(prefix + ref).replace(pathResolveRegex,'$2$3')]; |
|
}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 |
|
// if there is a path, we will iterate through the path references |
|
if(path[3]){ |
|
path[3].replace(/(\[([^\]]+)\])|(\.?([^\.\[]+))/g,function(t,a,b,c,d){ |
|
ref = ref && ref[b ? b.replace(/[\"\'\\]/,'') : d]; |
|
}); |
|
} |
|
} |
|
if(ref){ |
|
val = ref; |
|
}else{ |
|
// 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 |
|
if(!stop){ |
|
var rewalking; |
|
if(!rewalking){ |
|
reWalk.push(target); // we need to rewalk it to resolve references |
|
} |
|
rewalking = true; // we only want to add it once |
|
// TODO: What is propertyDefinition supposed to be here? |
|
val = walk(val, false, val[refAttribute], true, propertyDefinition); |
|
// create a lazy loaded object |
|
val._loadObject = args.loader; |
|
} |
|
} |
|
}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 |
|
val = walk( |
|
val, |
|
reWalk==it, |
|
id === undefined ? undefined : addProp(id, i), // the default id to use |
|
false, |
|
// TODO: What is propertyDefinition supposed to be here? |
|
propertyDefinition, |
|
// if we have an existing object child, we want to |
|
// maintain it's identity, so we pass it as the default object |
|
target != it && typeof target[i] == 'object' && target[i], |
|
it |
|
); |
|
} |
|
} |
|
} |
|
it[i] = val; |
|
if(target!=it && !target.__isDirty){// do updates if we are updating an existing object and it's not dirty |
|
var old = target[i]; |
|
target[i] = val; // only update if it changed |
|
if(update && val !== old && // see if it is different |
|
!target._loadObject && // no updates if we are just lazy loading |
|
!(i.charAt(0) == '_' && i.charAt(1) == '_') && i != "$ref" && |
|
!(val instanceof Date && old instanceof Date && val.getTime() == old.getTime()) && // make sure it isn't an identical date |
|
!(typeof val == 'function' && typeof old == 'function' && val.toString() == old.toString()) && // make sure it isn't an indentical function |
|
index.onUpdate){ |
|
index.onUpdate(target,i,old,val); // call the listener for each update |
|
} |
|
} |
|
} |
|
} |
|
|
|
if(update && (idAttribute in it || target instanceof Array)){ |
|
// this means we are updating with a full representation of the object, we need to remove deleted |
|
for(i in target){ |
|
if(!target.__isDirty && target.hasOwnProperty(i) && !it.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && !(target instanceof Array && isNaN(i))){ |
|
if(index.onUpdate && i != "_loadObject" && i != "_idAttr"){ |
|
index.onUpdate(target,i,target[i],undefined); // call the listener for each update |
|
} |
|
delete target[i]; |
|
while(target instanceof Array && target.length && target[target.length-1] === undefined){ |
|
// shorten the target if necessary |
|
target.length--; |
|
} |
|
} |
|
} |
|
}else{ |
|
if(index.onLoad){ |
|
index.onLoad(target); |
|
} |
|
} |
|
return target; |
|
} |
|
if(root && typeof root == 'object'){ |
|
root = walk(root,false,args.defaultId, true); // do the main walk through |
|
walk(reWalk,false); // re walk any parts that were not able to resolve references on the first round |
|
} |
|
return root; |
|
}, |
|
|
|
|
|
fromJson: function(/*String*/ str,/*Object?*/ args){ |
|
// summary: |
|
// evaluates the passed string-form of a JSON object. |
|
// str: |
|
// a string literal of a JSON item, for instance: |
|
// | '{ "foo": [ "bar", 1, { "baz": "thud" } ] }' |
|
// args: |
|
// See resolveJson |
|
// returns: |
|
// An object, the result of the evaluation |
|
function ref(target){ // support call styles references as well |
|
var refObject = {}; |
|
refObject[this.refAttribute] = target; |
|
return refObject; |
|
} |
|
var root; |
|
try{ |
|
root = eval('(' + str + ')'); // do the eval |
|
}catch(e){ |
|
throw new SyntaxError("Invalid JSON string: " + e.message + " parsing: "+ str); |
|
} |
|
if(root){ |
|
return this.resolveJson(root, args); |
|
} |
|
return root; |
|
}, |
|
|
|
toJson: function(/*Object*/ it, /*Boolean?*/ prettyPrint, /*Object?*/ idPrefix, /*Object?*/ indexSubObjects, /*String*/idAttribute){ |
|
// 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(). |
|
// idPrefix: |
|
// The prefix that has been used for the absolute ids |
|
// returns: |
|
// a String representing the serialized version of the passed object. |
|
var useRefs = this._useRefs; |
|
var addProp = this._addProp; |
|
var refAttribute = this.refAttribute; |
|
idPrefix = idPrefix || ''; // the id prefix for this context |
|
var paths={}; |
|
var generated = {}; |
|
var idAttributeProvided = idAttribute ? true: false; |
|
var uniqueIdCount = 1; |
|
function getUniqueId() { |
|
var str = (uniqueIdCount++).toString(); |
|
if (generated.hasOwnProperty(str)) { |
|
return getUniqueId(); |
|
} |
|
return str; |
|
} |
|
function serialize(it,path,_indentStr){ |
|
if(typeof it == 'object' && it){ |
|
var value; |
|
if(it instanceof Date){ // properly serialize dates |
|
return '"' + 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 != '#' && ((useRefs && !id.match(/#/)) || paths[id])){ |
|
var ref = id; |
|
if(id.charAt(0)!='#'){ |
|
if(it.__clientId == id){ |
|
ref = "cid:" + id; |
|
}else if(id.substring(0, idPrefix.length) == idPrefix){ // see if the reference is in the current context |
|
// a reference with a prefix matching the current context, the prefix should be removed |
|
ref = id.substring(idPrefix.length); |
|
}else{ |
|
// a reference to a different context, assume relative url based referencing |
|
ref = id; |
|
} |
|
} |
|
var refObject = {}; |
|
refObject[refAttribute] = ref; |
|
return djson.toJson(refObject, prettyPrint); |
|
} |
|
path = id; |
|
}else{ |
|
if (idAttributeProvided) { |
|
if(!(it instanceof Array)){ |
|
path = getUniqueId(); |
|
it.__id = path; // we will create path ids for other objects in case they are circular |
|
generated[path] = it; |
|
} |
|
} else { |
|
it.__id = path; // we will create path ids for other objects in case they are circular |
|
generated[path] = it; |
|
} |
|
} |
|
paths[path] = it;// save it here so they can be deleted at the end |
|
_indentStr = _indentStr || ""; |
|
var nextIndent = prettyPrint ? _indentStr + djson.toJsonIndentStr : ""; |
|
var newLine = prettyPrint ? "\n" : ""; |
|
var sep = prettyPrint ? " " : ""; |
|
|
|
if(it instanceof Array){ |
|
var res = array.map(it, function(obj,i){ |
|
var val = serialize(obj, idAttributeProvided ? undefined : addProp(path, i), nextIndent); |
|
if(typeof val != "string"){ |
|
val = "undefined"; |
|
} |
|
return newLine + nextIndent + val; |
|
}); |
|
return "[" + res.join("," + sep) + newLine + _indentStr + "]"; |
|
} |
|
|
|
var output = []; |
|
if (idAttributeProvided && (typeof it[idAttribute] === 'undefined')) { |
|
output.push(newLine + nextIndent + djson._escapeString(idAttribute) + ":" + sep + djson.toJson(it.__id)); |
|
} |
|
for(var i in it){ |
|
if(it.hasOwnProperty(i)){ |
|
var keyStr; |
|
if(typeof i == "number"){ |
|
keyStr = '"' + i + '"'; |
|
}else if(typeof i == "string" && (i.charAt(0) != '_' || i.charAt(1) != '_')){ |
|
// we don't serialize our internal properties __id and __clientId |
|
keyStr = djson._escapeString(i); |
|
}else{ |
|
// skip non-string or number keys |
|
continue; |
|
} |
|
var val = serialize(it[i],idAttributeProvided ? undefined : addProp(path, i),nextIndent); |
|
if(typeof val != "string"){ |
|
// skip non-serializable values |
|
continue; |
|
} |
|
output.push(newLine + nextIndent + keyStr + ":" + sep + val); |
|
} |
|
} |
|
return "{" + output.join("," + sep) + newLine + _indentStr + "}"; |
|
}else if(typeof it == "function" && dojox.json.ref.serializeFunctions){ |
|
return it.toString(); |
|
} |
|
|
|
return djson.toJson(it); // use the default serializer for primitives |
|
} |
|
var json = serialize(it,idAttributeProvided ? undefined : '#',''); |
|
if(!indexSubObjects){ |
|
for(var i in generated) {// cleanup the temporary path-generated ids |
|
delete generated[i].__id; |
|
} |
|
} |
|
return json; |
|
}, |
|
_addProp: function(id, prop){ |
|
return id + (id.match(/#/) ? id.length == 1 ? '' : '.' : '#') + prop; |
|
}, |
|
// refAttribute: String |
|
// This indicates what property is the reference property. This acts like the idAttribute |
|
// except that this is used to indicate the current object is a reference or only partially |
|
// loaded. This defaults to "$ref". |
|
refAttribute: "$ref", |
|
_useRefs: false, |
|
serializeFunctions: false |
|
}; |
|
});
|
|
|