UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

471 lines (414 loc) 16.8 kB
/* ************************************************************************ 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!"); } } });