@qooxdoo/framework
Version:
The JS Framework for Coders
471 lines (414 loc) • 16.8 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2009 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Martin Wittemann (martinwittemann)
************************************************************************ */
/**
* This class is responsible for converting json data to class instances
* including the creation of the classes.
* To retrieve the native data of created models use the methods
* described in {@link qx.util.Serializer}.
*/
qx.Class.define("qx.data.marshal.Json",
{
extend : qx.core.Object,
implement : [qx.data.marshal.IMarshaler],
/**
* @param delegate {Object} An object containing one of the methods described
* in {@link qx.data.marshal.IMarshalerDelegate}.
*/
construct : function(delegate)
{
this.base(arguments);
this.__delegate = delegate;
},
statics :
{
$$instance : null,
/**
* Creates a qooxdoo object based on the given json data. This function
* is just a static wrapper. If you want to configure the creation
* process of the class, use {@link qx.data.marshal.Json} directly.
*
* @param data {Object} The object for which classes should be created.
* @param includeBubbleEvents {Boolean} Whether the model should support
* the bubbling of change events or not.
*
* @return {qx.core.Object} An instance of the corresponding class.
*/
createModel : function(data, includeBubbleEvents) {
// singleton for the json marshaler
if (this.$$instance === null) {
this.$$instance = new qx.data.marshal.Json();
}
// be sure to create the classes first
this.$$instance.toClass(data, includeBubbleEvents);
// return the model
return this.$$instance.toModel(data);
},
/**
* Legacy json hash method used as default in Qooxdoo < v6.0.
* You can go back to the old behaviour like this:
*
* <code>
* var marshaller = new qx.data.marshal.Json({
* getJsonHash: qx.data.marshal.Json.legacyJsonHash
* });
* </code>
*/
legacyJsonHash: function (data, includeBubbleEvents) {
return Object.keys(data).sort().join('"')
+ (includeBubbleEvents===true ? "♥" : "");
}
},
members :
{
__delegate : null,
/**
* Converts a given object into a hash which will be used to identify the
* classes under the namespace <code>qx.data.model</code>.
*
* @param data {Object} The JavaScript object from which the hash is
* required.
* @param includeBubbleEvents {Boolean?false} Whether the model should
* support the bubbling of change events or not.
* @return {String} The hash representation of the given JavaScript object.
*/
__jsonToHash : function (data, includeBubbleEvents)
{
if (this.__delegate && this.__delegate.getJsonHash) {
return this.__delegate.getJsonHash(data, includeBubbleEvents);
}
return Object.keys(data).sort().join('|')
+ (includeBubbleEvents===true ? "♥" : "");
},
/**
* Get the "most enhanced" hash for a given object. That is the hash for
* the class that is most feature rich in respect of the bubble event
* feature. If there are two equal classes available (defined), one with
* and one without the bubble event feature, this method will return the
* hash of the class that includes the bubble event.
*
* @param data {Object} The JavaScript object from which the hash is
* required.
* @param includeBubbleEvents {Boolean} Whether the preferred model should
* support the bubbling of change events or not.
* If <code>null</code>, an automatic selection will take place which
* selects the "best" model currently available.
* @return {String} The hash representation of the given JavaScript object.
*/
__jsonToBestHash : function (data, includeBubbleEvents)
{
// forced mode?
//
if (includeBubbleEvents === true) {
return this.__jsonToHash(data, true);
}
if (includeBubbleEvents === false) {
return this.__jsonToHash(data, false);
}
// automatic mode!
//
var hash = this.__jsonToHash(data); // without bubble event feature
var bubbleClassHash = hash + "♥"; // with bubble event feature
var bubbleClassName = "qx.data.model." + bubbleClassHash;
// In case there's a class with bubbling, we *always* prefer that one!
return qx.Class.isDefined(bubbleClassName) ? bubbleClassHash : hash;
},
/**
* Creates for the given data the needed classes. The classes contain for
* every key in the data a property. The classname is always the prefix
* <code>qx.data.model</code> and the hash of the data created by
* {@link #__jsonToHash}. Two objects containing the same keys will not
* create two different classes. The class creation process also supports
* the functions provided by its delegate.
*
* Important, please keep in mind that only valid JavaScript identifiers
* can be used as keys in the data map. For convenience '-' in keys will
* be removed (a-b will be ab in the end).
*
* @see qx.data.store.IStoreDelegate
*
* @param data {Object} The object for which classes should be created.
* @param includeBubbleEvents {Boolean} Whether the model should support
* the bubbling of change events or not.
*/
toClass: function(data, includeBubbleEvents) {
this.__toClass(data, includeBubbleEvents, null, 0);
},
/**
* Implementation of {@link #toClass} used for recursion.
*
* @param data {Object} The object for which classes should be created.
* @param includeBubbleEvents {Boolean} Whether the model should support
* the bubbling of change events or not.
* @param parentProperty {String|null} The name of the property the
* data will be stored in.
* @param depth {Number} The depth of the data relative to the data's root.
*/
__toClass : function(data, includeBubbleEvents, parentProperty, depth) {
// break on all primitive json types and qooxdoo objects
if (
!qx.lang.Type.isObject(data)
|| !!data.$$isString // check for localized strings
|| data instanceof qx.core.Object
) {
// check for arrays
if (data instanceof Array || qx.Bootstrap.getClass(data) == "Array") {
for (var i = 0; i < data.length; i++) {
this.__toClass(data[i], includeBubbleEvents, parentProperty + "[" + i + "]", depth+1);
}
}
// ignore arrays and primitive types
return;
}
var hash = this.__jsonToHash(data, includeBubbleEvents);
// ignore rules
if (this.__ignore(hash, parentProperty, depth)) {
return;
}
// check for the possible child classes
for (var key in data) {
this.__toClass(data[key], includeBubbleEvents, key, depth+1);
}
// class already exists
if (qx.Class.isDefined("qx.data.model." + hash)) {
return;
}
// class is defined by the delegate
if (
this.__delegate
&& this.__delegate.getModelClass
&& this.__delegate.getModelClass(hash, data, parentProperty, depth) != null
) {
return;
}
// create the properties map
var properties = {};
// include the disposeItem for the dispose process.
var members = {__disposeItem : this.__disposeItem};
for (var key in data) {
// apply the property names mapping
if (this.__delegate && this.__delegate.getPropertyMapping) {
key = this.__delegate.getPropertyMapping(key, hash);
}
// strip the unwanted characters
key = key.replace(/-|\.|\s+/g, "");
// check for valid JavaScript identifier (leading numbers are ok)
if (qx.core.Environment.get("qx.debug")) {
this.assertTrue((/^[$0-9A-Za-z_]*$/).test(key),
"The key '" + key + "' is not a valid JavaScript identifier.");
}
properties[key] = {};
properties[key].nullable = true;
properties[key].event = "change" + qx.lang.String.firstUp(key);
// bubble events
if (includeBubbleEvents) {
properties[key].apply = "_applyEventPropagation";
}
// validation rules
if (this.__delegate && this.__delegate.getValidationRule) {
var rule = this.__delegate.getValidationRule(hash, key);
if (rule) {
properties[key].validate = "_validate" + key;
members["_validate" + key] = rule;
}
}
}
// try to get the superclass, qx.core.Object as default
if (this.__delegate && this.__delegate.getModelSuperClass) {
var superClass =
this.__delegate.getModelSuperClass(hash, parentProperty, depth) || qx.core.Object;
} else {
var superClass = qx.core.Object;
}
// try to get the mixins
var mixins = [];
if (this.__delegate && this.__delegate.getModelMixins) {
var delegateMixins = this.__delegate.getModelMixins(hash, parentProperty, depth);
// check if its an array
if (!qx.lang.Type.isArray(delegateMixins)) {
if (delegateMixins != null) {
mixins = [delegateMixins];
}
} else {
mixins = delegateMixins;
}
}
// include the mixin for the event bubbling
if (includeBubbleEvents) {
mixins.push(qx.data.marshal.MEventBubbling);
}
// create the map for the class
var newClass = {
extend : superClass,
include : mixins,
properties : properties,
members : members
};
qx.Class.define("qx.data.model." + hash, newClass);
},
/**
* Helper for disposing items of the created class.
*
* @param item {var} The item to dispose.
*/
__disposeItem : function(item) {
if (!(item instanceof qx.core.Object)) {
// ignore all non objects
return;
}
// ignore already disposed items (could happen during shutdown)
if (item.isDisposed()) {
return;
}
item.dispose();
},
/**
* Creates an instance for the given data hash.
*
* @param hash {String} The hash of the data for which an instance should
* be created.
* @param parentProperty {String|null} The name of the property the data
* will be stored in.
* @param depth {Number} The depth of the object relative to the data root.
* @param data {Map} The data for which an instance should be created.
* @return {qx.core.Object} An instance of the corresponding class.
*/
__createInstance : function (hash, data, parentProperty, depth)
{
var delegateClass;
// get the class from the delegate
if (this.__delegate && this.__delegate.getModelClass) {
delegateClass = this.__delegate.getModelClass(hash, data, parentProperty, depth);
}
if (delegateClass != null) {
return (new delegateClass());
} else {
var className = "qx.data.model." + hash;
var clazz = qx.Class.getByName(className);
if (!clazz)
{
// Extra check for possible bubble-event feature inconsistency
var noBubbleClassName = className.replace("♥", "");
if (qx.Class.getByName(noBubbleClassName))
{
throw new Error( "Class '" + noBubbleClassName + "' found, " +
"but it does not support changeBubble event." );
}
throw new Error("Class '" + className + "' could not be found.");
}
return (new clazz());
}
},
/**
* Helper to decide if the delegate decides to ignore a data set.
* @param hash {String} The property names.
* @param parentProperty {String|null} The name of the property the data
* will be stored in.
* @param depth {Number} The depth of the object relative to the data root.
* @return {Boolean} <code>true</code> if the set should be ignored
*/
__ignore : function(hash, parentProperty, depth) {
var del = this.__delegate;
return del && del.ignore && del.ignore(hash, parentProperty, depth);
},
/**
* Creates for the given data the needed models. Be sure to have the classes
* created with {@link #toClass} before calling this method. The creation
* of the class itself is delegated to the {@link #__createInstance} method,
* which could use the {@link qx.data.store.IStoreDelegate} methods, if
* given.
*
* @param data {Object} The object for which models should be created.
* @param includeBubbleEvents {Boolean?null} Whether the model should
* support the bubbling of change events or not.
* If omitted or <code>null</code>, an automatic selection will take place
* which selects the "best" model currently available.
* @return {qx.core.Object} The created model object.
*/
toModel : function (data, includeBubbleEvents) {
return this.__toModel(data, includeBubbleEvents, null, 0);
},
/**
* Implementation of {@link #toModel} used for recursion.
*
* @param data {Object} The object for which models should be created.
* @param includeBubbleEvents {Boolean|null} Whether the model should
* support the bubbling of change events or not.
* If <code>null</code>, an automatic selection will take place which
* selects the "best" model currently available.
* @param parentProperty {String|null} The name of the property the
* data will be stored in.
* @param depth {Number} The depth of the data relative to the data's root.
* @return {qx.core.Object} The created model object.
*/
__toModel : function (data, includeBubbleEvents, parentProperty, depth)
{
var isObject = qx.lang.Type.isObject(data);
var isArray = data instanceof Array || qx.Bootstrap.getClass(data) == "Array";
if (
(!isObject && !isArray)
|| !!data.$$isString // check for localized strings
|| data instanceof qx.core.Object
) {
return data;
// ignore rules
} else if (this.__ignore(this.__jsonToBestHash(data, includeBubbleEvents), parentProperty, depth)) {
return data;
} else if (isArray) {
var arrayClass = qx.data.Array;
if (this.__delegate && this.__delegate.getArrayClass) {
var customArrayClass = this.__delegate.getArrayClass(parentProperty, depth);
arrayClass = customArrayClass || arrayClass;
}
var array = new arrayClass();
// set the auto dispose for the array
array.setAutoDisposeItems(true);
for (var i = 0; i < data.length; i++) {
array.push(this.__toModel(data[i], includeBubbleEvents, parentProperty + "[" + i + "]", depth+1));
}
return array;
} else if (isObject) {
// create an instance for the object
var hash = this.__jsonToBestHash(data, includeBubbleEvents);
var model = this.__createInstance(hash, data, parentProperty, depth);
// go threw all element in the data
for (var key in data) {
// apply the property names mapping
var propertyName = key;
if (this.__delegate && this.__delegate.getPropertyMapping) {
propertyName = this.__delegate.getPropertyMapping(key, hash);
}
var propertyNameReplaced = propertyName.replace(/-|\.|\s+/g, "");
// warn if there has been a replacement
if (
(qx.core.Environment.get("qx.debug")) &&
qx.core.Environment.get("qx.debug.databinding")
) {
if (propertyNameReplaced != propertyName) {
this.warn(
"The model contained an illegal name: '" + key +
"'. Replaced it with '" + propertyName + "'."
);
}
}
propertyName = propertyNameReplaced;
// only set the properties if they are available [BUG #5909]
var setterName = "set" + qx.lang.String.firstUp(propertyName);
if (model[setterName]) {
model[setterName](this.__toModel(data[key], includeBubbleEvents, key, depth+1));
}
}
return model;
}
throw new Error("Unsupported type!");
}
}
});