@qooxdoo/framework
Version:
The JS Framework for Coders
1,585 lines (1,354 loc) • 57.2 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 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:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
* John Spackman (john.spackman@zenesis.com)
************************************************************************ */
/**
* This class is one of the most important parts of qooxdoo's
* object-oriented features.
*
* Its {@link #define} method is used to create qooxdoo classes.
*
* Each instance of a class defined by {@link #define} has
* the following keys attached to the constructor and the prototype:
*
* <table>
* <tr><th><code>classname</code></th><td>The fully-qualified name of the class (e.g. <code>"qx.ui.core.Widget"</code>).</td></tr>
* <tr><th><code>basename</code></th><td>The namespace part of the class name (e.g. <code>"qx.ui.core"</code>).</td></tr>
* <tr><th><code>constructor</code></th><td>A reference to the constructor of the class.</td></tr>
* <tr><th><code>superclass</code></th><td>A reference to the constructor of the super class.</td></tr>
* </table>
*
* Each method may access static members of the same class by using
* <code>this.self(arguments)</code> ({@link qx.core.Object#self}):
* <pre class='javascript'>
* statics : { FOO : "bar" },
* members: {
* baz: function(x) {
* this.self(arguments).FOO;
* ...
* }
* }
* </pre>
*
* Each overriding method may call the overridden method by using
* <code>this.base(arguments [, ...])</code> ({@link qx.core.Object#base}). This is also true for calling
* the constructor of the superclass.
* <pre class='javascript'>
* members: {
* foo: function(x) {
* this.base(arguments, x);
* ...
* }
* }
* </pre>
*
* By using <code>qx.Class</code> within an app, the native JS data types are
* conveniently polyfilled according to {@link qx.lang.normalize}.
*
* Annotations can be added to classes, constructors, destructors, and methods, properties, and statics -
* see <code>qx.Annotation</code> for examples and means access annotations at runtime.
*
* @require(qx.Interface)
* @require(qx.Mixin)
* @require(qx.lang.normalize.Array)
* @require(qx.lang.normalize.Date)
* @require(qx.lang.normalize.Error)
* @require(qx.lang.normalize.Function)
* @require(qx.lang.normalize.String)
* @require(qx.lang.normalize.Object)
* @require(qx.lang.normalize.Number)
*/
qx.Bootstrap.define("qx.Class",
{
statics :
{
/**
* A static reference to the property implementation in the case it
* should be included.
*/
__Property : qx.core.Environment.get("module.property") ? qx.core.Property : null,
/*
---------------------------------------------------------------------------
PUBLIC METHODS
---------------------------------------------------------------------------
*/
/**
* Define a new class using the qooxdoo class system. This sets up the
* namespace for the class and generates the class from the definition map.
*
* Example:
* <pre class='javascript'>
* qx.Class.define("name",
* {
* extend : Object, // superclass
* implement : [Interfaces],
* include : [Mixins],
*
* statics:
* {
* CONSTANT : 3.141,
*
* publicMethod: function() {},
* _protectedMethod: function() {},
* __privateMethod: function() {}
* },
*
* properties:
* {
* "tabIndex": { check: "Number", init : -1 }
* },
*
* members:
* {
* publicField: "foo",
* publicMethod: function() {},
*
* _protectedField: "bar",
* _protectedMethod: function() {},
*
* __privateField: "baz",
* __privateMethod: function() {}
* }
* });
* </pre>
*
* @param name {String?null} Name of the class. If <code>null</code>, the class
* will not be added to any namespace which could be handy for testing.
* @param config {Map ? null} Class definition structure. The configuration map has the following keys:
* <table>
* <tr><th>Name</th><th>Type</th><th>Description</th></tr>
* <tr><th>type</th><td>String</td><td>
* Type of the class. Valid types are "abstract", "static" and "singleton".
* If unset it defaults to a regular non-static class.
* </td></tr>
* <tr><th>extend</th><td>Class</td><td>The super class the current class inherits from.</td></tr>
* <tr><th>implement</th><td>Interface | Interface[]</td><td>Single interface or array of interfaces the class implements.</td></tr>
* <tr><th>include</th><td>Mixin | Mixin[]</td><td>Single mixin or array of mixins, which will be merged into the class.</td></tr>
* <tr><th>construct</th><td>Function</td><td>The constructor of the class.</td></tr>
* <tr><th>statics</th><td>Map</td><td>Map of static members of the class.</td></tr>
* <tr><th>properties</th><td>Map</td><td>Map of property definitions. For a description of the format of a property definition see
* {@link qx.core.Property}.</td></tr>
* <tr><th>members</th><td>Map</td><td>Map of instance members of the class.</td></tr>
* <tr><th>environment</th><td>Map</td><td>Map of environment settings for this class. For a description of the format of a setting see
* {@link qx.core.Environment}.</td></tr>
* <tr><th>events</th><td>Map</td><td>
* Map of events the class fires. The keys are the names of the events and the values are the
* corresponding event type class names.
* </td></tr>
* <tr><th>defer</th><td>Function</td><td>Function that is called at the end of processing the class declaration. It allows access to the declared statics, members and properties.</td></tr>
* <tr><th>destruct</th><td>Function</td><td>The destructor of the class.</td></tr>
* </table>
* @return {Class} The defined class
*/
define : function(name, config)
{
if (!config) {
config = {};
}
// Normalize include to array
if (config.include && !(qx.Bootstrap.getClass(config.include) === "Array")) {
config.include = [config.include];
}
// Normalize implement to array
if (config.implement && !(qx.Bootstrap.getClass(config.implement) === "Array")) {
config.implement = [config.implement];
}
// Normalize type
var implicitType = false;
if (!config.hasOwnProperty("extend") && !config.type) {
config.type = "static";
implicitType = true;
}
// Validate incoming data
if (qx.core.Environment.get("qx.debug")) {
try {
this.__validateConfig(name, config);
} catch(ex) {
if (implicitType) {
ex.message = 'Assumed static class because no "extend" key was found. ' + ex.message;
}
throw ex;
}
}
// Create the class
var clazz = this.__createClass(name, config.type, config.extend, config.statics, config.construct, config.destruct, config.include);
// Initialise class and constructor/destructor annotations
[ "@", "@construct", "@destruct" ].forEach(function(id) {
this.__attachAnno(clazz, id, null, config[id]);
}, this);
// Members, properties, events and mixins are only allowed for non-static classes
if (config.extend)
{
// Attach properties
if (config.properties) {
this.__addProperties(clazz, config.properties, true);
}
// Attach members
if (config.members) {
this.__addMembers(clazz, config.members, true, true, false);
}
// Process events
if (config.events) {
this.__addEvents(clazz, config.events, true);
}
// Include mixins
// Must be the last here to detect conflicts
if (config.include)
{
for (var i=0, l=config.include.length; i<l; i++) {
this.__addMixin(clazz, config.include[i], false);
}
}
}
// If config has a 'extend' key but it's null or undefined
else if (config.hasOwnProperty('extend') && qx.core.Environment.get("qx.debug"))
{
throw new Error('"extend" parameter is null or undefined');
}
// Process environment
if (config.environment)
{
for (var key in config.environment) {
qx.core.Environment.add(key, config.environment[key]);
}
}
// Interface support for non-static classes
if (config.implement)
{
for (var i=0, l=config.implement.length; i<l; i++) {
this.__addInterface(clazz, config.implement[i]);
}
}
if (qx.core.Environment.get("qx.debug")) {
this.__validateAbstractInterfaces(clazz);
}
// Process defer
if (config.defer)
{
config.defer.self = clazz;
qx.Bootstrap.addPendingDefer(clazz, function() {
config.defer(clazz, clazz.prototype,
{
add : function(name, config)
{
// build pseudo properties map
var properties = {};
properties[name] = config;
// execute generic property handler
qx.Class.__addProperties(clazz, properties, true);
}
});
});
}
return clazz;
},
/**
* Removes a class from qooxdoo defined by {@link #define}
*
* @param name {String} Name of the class
*/
undefine : function(name)
{
// first, delete the class from the registry
delete this.$$registry[name];
// delete the class reference from the namespaces and all empty namespaces
var ns = name.split(".");
// build up an array containing all namespace objects including window
var objects = [window];
for (var i = 0; i < ns.length; i++) {
objects.push(objects[i][ns[i]]);
}
// go through all objects and check for the constructor or empty namespaces
for (var i = objects.length - 1; i >= 1; i--) {
var last = objects[i];
var parent = objects[i - 1];
if (qx.Bootstrap.isFunction(last) || qx.Bootstrap.objectGetLength(last) === 0) {
delete parent[ns[i - 1]];
} else {
break;
}
}
},
/**
* Whether the given class exists
*
* @signature function(name)
* @param name {String} class name to check
* @return {Boolean} true if class exists
*/
isDefined : qx.util.OOUtil.classIsDefined,
/**
* Determine the total number of classes
*
* @return {Number} the total number of classes
*/
getTotalNumber : function() {
return qx.Bootstrap.objectGetLength(this.$$registry);
},
/**
* Find a class by its name
*
* @signature function(name)
* @param name {String} class name to resolve
* @return {Class} the class
*/
getByName : qx.Bootstrap.getByName,
/**
* Include all features of the given mixin into the class. The mixin must
* not include any methods or properties that are already available in the
* class. This would only be possible using the {@link #patch} method.
*
* @param clazz {Class} An existing class which should be augmented by including a mixin.
* @param mixin {Mixin} The mixin to be included.
*/
include : function(clazz, mixin)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!mixin) {
throw new Error("The mixin to include into class '" + clazz.classname + "' is undefined/null!");
}
qx.Mixin.isCompatible(mixin, clazz);
}
qx.Class.__addMixin(clazz, mixin, false);
},
/**
* Include all features of the given mixin into the class. The mixin may
* include features, which are already defined in the target class. Existing
* features of equal name will be overwritten.
* Please keep in mind that this functionality is not intended for regular
* use, but as a formalized way (and a last resort) in order to patch
* existing classes.
*
* <b>WARNING</b>: You may break working classes and features.
*
* @param clazz {Class} An existing class which should be modified by including a mixin.
* @param mixin {Mixin} The mixin to be included.
*/
patch : function(clazz, mixin)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!mixin) {
throw new Error("The mixin to patch class '" + clazz.classname + "' is undefined/null!");
}
qx.Mixin.isCompatible(mixin, clazz);
}
qx.Class.__addMixin(clazz, mixin, true);
},
/**
* Detects whether the object is a Class (and not an instance of a class)
*
* @param obj {Object?} the object to inspect
* @return {Boolean} true if it is a class, false if it is anything else
*/
isClass: function(obj) {
return obj && obj.$$type === "Class" && obj.constructor === obj;
},
/**
* Whether a class is a direct or indirect sub class of another class,
* or both classes coincide.
*
* @param clazz {Class} the class to check.
* @param superClass {Class} the potential super class
* @return {Boolean} whether clazz is a sub class of superClass.
*/
isSubClassOf : function(clazz, superClass)
{
if (!clazz) {
return false;
}
if (clazz == superClass) {
return true;
}
if (clazz.prototype instanceof superClass) {
return true;
}
return false;
},
/**
* Returns the definition of the given property. Returns null
* if the property does not exist.
*
* @signature function(clazz, name)
* @param clazz {Class} class to check
* @param name {String} name of the class to check for
* @return {Map|null} whether the object support the given event.
*/
getPropertyDefinition : qx.util.OOUtil.getPropertyDefinition,
/**
* Returns a list of all properties supported by the given class
*
* @param clazz {Class} Class to query
* @return {String[]} List of all property names
*/
getProperties : function(clazz)
{
var list = [];
while (clazz)
{
if (clazz.$$properties) {
list.push.apply(list, Object.keys(clazz.$$properties));
}
clazz = clazz.superclass;
}
return list;
},
/**
* Returns the class or one of its superclasses which contains the
* declaration for the given property in its class definition. Returns null
* if the property is not specified anywhere.
*
* @param clazz {Class} class to look for the property
* @param name {String} name of the property
* @return {Class | null} The class which includes the property
*/
getByProperty : function(clazz, name)
{
while (clazz)
{
if (clazz.$$properties && clazz.$$properties[name]) {
return clazz;
}
clazz = clazz.superclass;
}
return null;
},
/**
* Whether a class has the given property
*
* @signature function(clazz, name)
* @param clazz {Class} class to check
* @param name {String} name of the property to check for
* @return {Boolean} whether the class includes the given property.
*/
hasProperty : qx.util.OOUtil.hasProperty,
/**
* Returns the event type of the given event. Returns null if
* the event does not exist.
*
* @signature function(clazz, name)
* @param clazz {Class} class to check
* @param name {String} name of the event
* @return {String|null} Event type of the given event.
*/
getEventType : qx.util.OOUtil.getEventType,
/**
* Whether a class supports the given event type
*
* @signature function(clazz, name)
* @param clazz {Class} class to check
* @param name {String} name of the event to check for
* @return {Boolean} whether the class supports the given event.
*/
supportsEvent : qx.util.OOUtil.supportsEvent,
/**
* Whether a class directly includes a mixin.
*
* @param clazz {Class} class to check
* @param mixin {Mixin} the mixin to check for
* @return {Boolean} whether the class includes the mixin directly.
*/
hasOwnMixin : function(clazz, mixin) {
return clazz.$$includes && clazz.$$includes.indexOf(mixin) !== -1;
},
/**
* Returns the class or one of its superclasses which contains the
* declaration for the given mixin. Returns null if the mixin is not
* specified anywhere.
*
* @param clazz {Class} class to look for the mixin
* @param mixin {Mixin} mixin to look for
* @return {Class | null} The class which directly includes the given mixin
*/
getByMixin : function(clazz, mixin)
{
var list, i, l;
while (clazz)
{
if (clazz.$$includes)
{
list = clazz.$$flatIncludes;
for (i=0, l=list.length; i<l; i++)
{
if (list[i] === mixin) {
return clazz;
}
}
}
clazz = clazz.superclass;
}
return null;
},
/**
* Returns a list of all mixins available in a given class.
*
* @signature function(clazz)
* @param clazz {Class} class which should be inspected
* @return {Mixin[]} array of mixins this class uses
*/
getMixins : qx.util.OOUtil.getMixins,
/**
* Whether a given class or any of its superclasses includes a given mixin.
*
* @param clazz {Class} class to check
* @param mixin {Mixin} the mixin to check for
* @return {Boolean} whether the class includes the mixin.
*/
hasMixin: function(clazz, mixin) {
return !!this.getByMixin(clazz, mixin);
},
/**
* Whether a given class directly includes an interface.
*
* This function will only return "true" if the interface was defined
* in the class declaration ({@link qx.Class#define}) using the "implement"
* key.
*
* @param clazz {Class} class or instance to check
* @param iface {Interface} the interface to check for
* @return {Boolean} whether the class includes the mixin directly.
*/
hasOwnInterface : function(clazz, iface) {
return clazz.$$implements && clazz.$$implements.indexOf(iface) !== -1;
},
/**
* Returns the class or one of its super classes which contains the
* declaration of the given interface. Returns null if the interface is not
* specified anywhere.
*
* @signature function(clazz, iface)
* @param clazz {Class} class to look for the interface
* @param iface {Interface} interface to look for
* @return {Class | null} the class which directly implements the given interface
*/
getByInterface : qx.util.OOUtil.getByInterface,
/**
* Returns a list of all interfaces a given class has to implement.
*
* @param clazz {Class} class which should be inspected
* @return {Interface[]} array of interfaces this class implements
*/
getInterfaces : function(clazz)
{
var list = [];
while (clazz)
{
if (clazz.$$implements) {
list.push.apply(list, clazz.$$flatImplements);
}
clazz = clazz.superclass;
}
return list;
},
/**
* Whether a given class or any of its super classes includes a given interface.
*
* This function will return "true" if the interface was defined
* in the class declaration ({@link qx.Class#define}) of the class
* or any of its super classes using the "implement"
* key.
*
* @signature function(clazz, iface)
* @param clazz {Class} class to check
* @param iface {Interface} the interface to check for
* @return {Boolean} whether the class includes the interface.
*/
hasInterface : qx.util.OOUtil.hasInterface,
/**
* Whether a given class complies to an interface.
*
* Checks whether all methods defined in the interface are
* implemented. The class does not need to implement
* the interface explicitly in the <code>extend</code> key.
*
* @param obj {Object} class to check
* @param iface {Interface} the interface to check for
* @return {Boolean} whether the class conforms to the interface.
*/
implementsInterface : function(obj, iface)
{
var clazz = obj.constructor;
if (this.hasInterface(clazz, iface)) {
return true;
}
if (qx.Interface.objectImplements(obj, iface)) {
return true;
}
if (qx.Interface.classImplements(clazz, iface)) {
return true;
}
return false;
},
/**
* Helper method to handle singletons
*
* @internal
* @return {Object} The singleton instance
*/
getInstance : function()
{
if (this.$$instance === null)
{
throw new Error("Singleton instance of " + this +
" is requested, but not ready yet. This is most likely due to a recursive call in the constructor path.");
}
if (!this.$$instance)
{
this.$$allowconstruct = true;
this.$$instance = null; // null means "object is being created"; needed for another call of getInstance() during instantiation
this.$$instance = new this();
delete this.$$allowconstruct;
}
return this.$$instance;
},
/**
* Retreive all subclasses of a given class
*
* @param clazz {Class} the class which should be inspected
*
* @return {Object} class name hash holding the references to the subclasses or null if the class does not exist.
*/
getSubclasses : function(clazz)
{
if(!clazz) {
return null;
}
var subclasses = {};
var registry = qx.Class.$$registry;
for (var name in registry) {
if(registry[name].superclass && registry[name].superclass == clazz) {
subclasses[name] = registry[name];
}
}
return subclasses;
},
/*
---------------------------------------------------------------------------
PRIVATE/INTERNAL BASICS
---------------------------------------------------------------------------
*/
/**
* This method will be attached to all classes to return
* a nice identifier for them.
*
* @internal
* @return {String} The class identifier
*/
genericToString : function() {
return "[Class " + this.classname + "]";
},
/** Stores all defined classes */
$$registry : qx.Bootstrap.$$registry,
/** @type {Map} allowed keys in non-static class definition */
__allowedKeys : qx.core.Environment.select("qx.debug",
{
"true":
{
"@" : "object",
"@construct" : "object",
"@destruct" : "object",
"type" : "string", // String
"extend" : "function", // Function
"implement" : "object", // Interface[]
"include" : "object", // Mixin[]
"construct" : "function", // Function
"statics" : "object", // Map
"properties" : "object", // Map
"members" : "object", // Map
"environment" : "object", // Map
"events" : "object", // Map
"defer" : "function", // Function
"destruct" : "function" // Function
},
"default" : null
}),
/** @type {Map} allowed keys in static class definition */
__staticAllowedKeys : qx.core.Environment.select("qx.debug",
{
"true":
{
"@" : "object",
"type" : "string", // String
"statics" : "object", // Map
"environment" : "object", // Map
"defer" : "function" // Function
},
"default" : null
}),
/**
* Validates an incoming configuration and checks for proper keys and values
*
* @signature function(name, config)
* @param name {String} The name of the class
* @param config {Map} Configuration map
*/
__validateConfig : qx.core.Environment.select("qx.debug",
{
"true": function(name, config)
{
// Validate type
if (config.type && !(config.type === "static" || config.type === "abstract" || config.type === "singleton")) {
throw new Error('Invalid type "' + config.type + '" definition for class "' + name + '"!');
}
// Validate non-static class on the "extend" key
if (config.type && config.type !== "static" && !config.extend) {
throw new Error('Invalid config in class "' + name + '"! Every non-static class has to extend at least the "qx.core.Object" class.');
}
// Validate keys
var allowed = config.type === "static" ? this.__staticAllowedKeys : this.__allowedKeys;
for (var key in config)
{
if (!allowed[key]) {
throw new Error('The configuration key "' + key + '" in class "' + name + '" is not allowed!');
}
if (config[key] == null) {
throw new Error('Invalid key "' + key + '" in class "' + name + '"! The value is undefined/null!');
}
if (typeof config[key] !== allowed[key]) {
throw new Error('Invalid type of key "' + key + '" in class "' + name + '"! The type of the key must be "' + allowed[key] + '"!');
}
}
// Validate maps
var maps = [ "statics", "properties", "members", "environment", "settings", "variants", "events" ];
for (var i=0, l=maps.length; i<l; i++)
{
var key = maps[i];
if (config[key] !== undefined && (
config[key].$$hash !== undefined || !qx.Bootstrap.isObject(config[key])
)) {
throw new Error('Invalid key "' + key + '" in class "' + name + '"! The value needs to be a map!');
}
}
// Validate include definition
if (config.include)
{
if (qx.Bootstrap.getClass(config.include) === "Array")
{
for (var i=0, a=config.include, l=a.length; i<l; i++)
{
if (a[i] == null || a[i].$$type !== "Mixin") {
throw new Error('The include definition in class "' + name + '" contains an invalid mixin at position ' + i + ': ' + a[i]);
}
}
}
else
{
throw new Error('Invalid include definition in class "' + name + '"! Only mixins and arrays of mixins are allowed!');
}
}
// Validate implement definition
if (config.implement)
{
if (qx.Bootstrap.getClass(config.implement) === "Array")
{
for (var i=0, a=config.implement, l=a.length; i<l; i++)
{
if (a[i] == null || a[i].$$type !== "Interface") {
throw new Error('The implement definition in class "' + name + '" contains an invalid interface at position ' + i + ': ' + a[i]);
}
}
}
else
{
throw new Error('Invalid implement definition in class "' + name + '"! Only interfaces and arrays of interfaces are allowed!');
}
}
// Check mixin compatibility
if (config.include)
{
try {
qx.Mixin.checkCompatibility(config.include);
} catch(ex) {
throw new Error('Error in include definition of class "' + name + '"! ' + ex.message);
}
}
// Validate environment
if (config.environment)
{
for (var key in config.environment)
{
if (key.substr(0, key.indexOf(".")) != name.substr(0, name.indexOf("."))) {
throw new Error('Forbidden environment setting "' + key +
'" found in "' + name + '". It is forbidden to define a ' +
'environment setting for an external namespace!');
}
}
}
// Validate settings
if (config.settings)
{
for (var key in config.settings)
{
if (key.substr(0, key.indexOf(".")) != name.substr(0, name.indexOf("."))) {
throw new Error('Forbidden setting "' + key + '" found in "' + name + '". It is forbidden to define a default setting for an external namespace!');
}
}
}
// Validate variants
if (config.variants)
{
for (var key in config.variants)
{
if (key.substr(0, key.indexOf(".")) != name.substr(0, name.indexOf("."))) {
throw new Error('Forbidden variant "' + key + '" found in "' + name + '". It is forbidden to define a variant for an external namespace!');
}
}
}
},
"default" : function(name, config) {}
}),
/**
* Validates the interfaces required by abstract base classes
*
* @signature function(clazz)
* @param clazz {Class} The configured class.
*/
__validateAbstractInterfaces : qx.core.Environment.select("qx.debug",
{
"true": function(clazz)
{
var superclass = clazz.superclass;
while (superclass)
{
if (superclass.$$classtype !== "abstract") {
break;
}
var interfaces = superclass.$$implements;
if (interfaces)
{
for (var i=0; i<interfaces.length; i++) {
qx.Interface.assert(clazz, interfaces[i], true);
}
}
superclass = superclass.superclass;
}
},
"default" : function(clazz) {}
}),
/**
* Attaches an annotation to a class
*
* @param clazz {Map} Static methods or fields
* @param group {String} Group name
* @param key {String} Name of the annotated item
* @param anno {Object} Annotation object
*/
__attachAnno : function(clazz, group, key, anno) {
if (anno !== undefined) {
if (clazz.$$annotations === undefined) {
clazz.$$annotations = {};
clazz.$$annotations[group] = {};
} else if (clazz.$$annotations[group] === undefined) {
clazz.$$annotations[group] = {};
}
if (!qx.lang.Type.isArray(anno)) {
anno = [anno];
}
if (key) {
clazz.$$annotations[group][key] = anno;
} else {
clazz.$$annotations[group] = anno;
}
}
},
/**
* Creates a class by type. Supports modern inheritance etc.
*
* @param name {String} Full name of the class
* @param type {String} type of the class, i.e. "static", "abstract" or "singleton"
* @param extend {Class} Superclass to inherit from
* @param statics {Map} Static methods or fields
* @param construct {Function} Constructor of the class
* @param destruct {Function} Destructor of the class
* @param mixins {Mixin[]} array of mixins of the class
* @return {Class} The generated class
*/
__createClass : function(name, type, extend, statics, construct, destruct, mixins)
{
var isStrictMode = function () {
return (typeof this == 'undefined');
};
var clazz;
if (!extend && qx.core.Environment.get("qx.aspects") == false)
{
// Create empty/non-empty class
clazz = statics || {};
qx.Bootstrap.setDisplayNames(clazz, name);
}
else
{
clazz = {};
if (extend)
{
// Create default constructor
if (!construct) {
construct = this.__createDefaultConstructor();
}
if (this.__needsConstructorWrapper(extend, mixins)) {
clazz = this.__wrapConstructor(construct, name, type);
} else {
clazz = construct;
}
// Add singleton getInstance()
if (type === "singleton") {
clazz.getInstance = this.getInstance;
}
qx.Bootstrap.setDisplayName(construct, name, "constructor");
}
// Copy statics
if (statics)
{
qx.Bootstrap.setDisplayNames(statics, name);
var key;
for (var i=0, a=Object.keys(statics), l=a.length; i<l; i++)
{
key = a[i];
var staticValue = statics[key];
if (qx.core.Environment.get("qx.debug")) {
if (key.charAt(0) === '@') {
if (statics[key.substring(1)] === undefined) {
throw new Error('Annonation for static "' + key.substring(1) + '" of Class "' + clazz.classname + '" does not exist!');
}
if (key.charAt(1) === "_" && key.charAt(2) === "_") {
throw new Error('Cannot annotate private static "' + key.substring(1) + '" of Class "' + clazz.classname);
}
}
}
if (key.charAt(0) === '@') {
continue;
}
if (qx.core.Environment.get("qx.aspects"))
{
if (staticValue instanceof Function) {
staticValue = qx.core.Aspect.wrap(name + "." + key, staticValue, "static");
}
clazz[key] = staticValue;
}
else
{
clazz[key] = staticValue;
}
// Attach annotations
this.__attachAnno(clazz, "statics", key, statics["@" + key]);
}
}
}
// Create namespace
var basename = name ? qx.Bootstrap.createNamespace(name, clazz) : "";
// Store names in constructor/object
clazz.classname = name;
if (!isStrictMode()) {
try {
clazz.name = name;
} catch(ex) {
// Nothing
}
}
clazz.basename = basename;
// Store type info
clazz.$$type = "Class";
if (type) {
clazz.$$classtype = type;
}
// Attach toString
if (!clazz.hasOwnProperty("toString")) {
clazz.toString = this.genericToString;
}
if (extend)
{
qx.Bootstrap.extendClass(clazz, construct, extend, name, basename);
// Store destruct onto class
if (destruct)
{
if (qx.core.Environment.get("qx.aspects")) {
destruct = qx.core.Aspect.wrap(name, destruct, "destructor");
}
clazz.$$destructor = destruct;
qx.Bootstrap.setDisplayName(destruct, name, "destruct");
}
}
// Store class reference in global class registry
this.$$registry[name] = clazz;
// Return final class object
return clazz;
},
/*
---------------------------------------------------------------------------
PRIVATE ADD HELPERS
---------------------------------------------------------------------------
*/
/**
* Attach events to the class
*
* @param clazz {Class} class to add the events to
* @param events {Map} map of event names the class fires.
* @param patch {Boolean ? false} Enable redefinition of event type?
*/
__addEvents : function(clazz, events, patch)
{
if (qx.core.Environment.get("qx.debug"))
{
if (typeof events !== "object" || qx.Bootstrap.getClass(events) === "Array") {
throw new Error(clazz.classname + ": the events must be defined as map!");
}
for (var key in events)
{
if (typeof events[key] !== "string") {
throw new Error(clazz.classname + "/" + key + ": the event value needs to be a string with the class name of the event object which will be fired.");
}
}
// Compare old and new event type/value if patching is disabled
if (clazz.$$events && patch !== true)
{
for (var key in events)
{
if (clazz.$$events[key] !== undefined && clazz.$$events[key] !== events[key]) {
throw new Error(clazz.classname + "/" + key + ": the event value/type cannot be changed from " + clazz.$$events[key] + " to " + events[key]);
}
}
}
}
if (clazz.$$events)
{
for (var key in events) {
clazz.$$events[key] = events[key];
}
}
else
{
clazz.$$events = events;
}
},
/**
* Attach properties to classes
*
* @param clazz {Class} class to add the properties to
* @param properties {Map} map of properties
* @param patch {Boolean ? false} Overwrite property with the limitations of a property
which means you are able to refine but not to replace (esp. for new properties)
*/
__addProperties : function(clazz, properties, patch)
{
// check for the property module
if (!qx.core.Environment.get("module.property")) {
throw new Error("Property module disabled.");
}
var config;
if (patch === undefined) {
patch = false;
}
var proto = clazz.prototype;
for (var name in properties)
{
config = properties[name];
// Check incoming configuration
if (qx.core.Environment.get("qx.debug")) {
this.__validateProperty(clazz, name, config, patch);
}
// Store name into configuration
config.name = name;
// Add config to local registry
if (!config.refine)
{
if (clazz.$$properties === undefined) {
clazz.$$properties = {};
}
clazz.$$properties[name] = config;
}
// Store init value to prototype. This makes it possible to
// overwrite this value in derived classes.
if (config.init !== undefined) {
clazz.prototype["$$init_" + name] = config.init;
}
// register event name
if (config.event !== undefined) {
// break if no events layer loaded
if (!qx.core.Environment.get("module.events")) {
throw new Error("Events module not enabled.");
}
var event = {};
event[config.event] = "qx.event.type.Data";
if (config.async) {
event[config.event + "Async"] = "qx.event.type.Data";
}
this.__addEvents(clazz, event, patch);
}
// Remember inheritable properties
if (config.inheritable)
{
this.__Property.$$inheritable[name] = true;
if (!proto.$$refreshInheritables) {
this.__Property.attachRefreshInheritables(clazz);
}
}
if (!config.refine) {
this.__Property.attachMethods(clazz, name, config);
}
// Add annotations
this.__attachAnno(clazz, "properties", name, config["@"]);
}
},
/**
* Validates the given property
*
* @signature function(clazz, name, config, patch)
* @param clazz {Class} class to add property to
* @param name {String} name of the property
* @param config {Map} configuration map
* @param patch {Boolean ? false} enable refine/patch?
*/
__validateProperty : qx.core.Environment.select("qx.debug",
{
"true": function(clazz, name, config, patch)
{
// check for properties
if (!qx.core.Environment.get("module.property")) {
throw new Error("Property module disabled.");
}
var has = this.hasProperty(clazz, name);
if (has)
{
var existingProperty = this.getPropertyDefinition(clazz, name);
if (config.refine && existingProperty.init === undefined) {
throw new Error("Could not refine an init value if there was previously no init value defined. Property '" + name + "' of class '" + clazz.classname + "'.");
}
}
if (!has && config.refine) {
throw new Error("Could not refine non-existent property: '" + name + "' of class: '" + clazz.classname + "'!");
}
if (has && !patch) {
throw new Error("Class " + clazz.classname + " already has a property: " + name + "!");
}
if (has && patch)
{
if (!config.refine) {
throw new Error('Could not refine property "' + name + '" without a "refine" flag in the property definition! This class: ' + clazz.classname + ', original class: ' + this.getByProperty(clazz, name).classname + '.');
}
for (var key in config)
{
if (key !== "init" && key !== "refine" && key !== "@") {
throw new Error("Class " + clazz.classname + " could not refine property: " + name + "! Key: " + key + " could not be refined!");
}
}
}
// Check 0.7 keys
var allowed = config.group ? this.__Property.$$allowedGroupKeys : this.__Property.$$allowedKeys;
for (var key in config)
{
if (allowed[key] === undefined) {
throw new Error('The configuration key "' + key + '" of property "' + name + '" in class "' + clazz.classname + '" is not allowed!');
}
if (config[key] === undefined) {
throw new Error('Invalid key "' + key + '" of property "' + name + '" in class "' + clazz.classname + '"! The value is undefined: ' + config[key]);
}
if (allowed[key] !== null && typeof config[key] !== allowed[key]) {
throw new Error('Invalid type of key "' + key + '" of property "' + name + '" in class "' + clazz.classname + '"! The type of the key must be "' + allowed[key] + '"!');
}
}
if (config.transform != null)
{
if (!(typeof config.transform === "string")) {
throw new Error('Invalid transform definition of property "' + name + '" in class "' + clazz.classname + '"! Needs to be a String.');
}
}
if (config.check != null)
{
if (
!qx.Bootstrap.isString(config.check) &&
!qx.Bootstrap.isArray(config.check) &&
!qx.Bootstrap.isFunction(config.check)
) {
throw new Error('Invalid check definition of property "' + name + '" in class "' + clazz.classname + '"! Needs to be a String, Array or Function.');
}
}
},
"default" : null
}),
/**
* Attach members to a class
*
* @param clazz {Class} clazz to add members to
* @param members {Map} The map of members to attach
* @param patch {Boolean ? false} Enable patching of
* @param base {Boolean ? true} Attach base flag to mark function as members
* of this class
* @param wrap {Boolean ? false} Whether the member method should be wrapped.
* this is needed to allow base calls in patched mixin members.
*/
__addMembers : function(clazz, members, patch, base, wrap)
{
var proto = clazz.prototype;
var key, member;
qx.Bootstrap.setDisplayNames(members, clazz.classname + ".prototype");
for (var i=0, a=Object.keys(members), l=a.length; i<l; i++)
{
key = a[i];
member = members[key];
if (qx.core.Environment.get("qx.debug"))
{
if (key.charAt(0) === '@') {
var annoKey = key.substring(1);
if (members[annoKey] === undefined && proto[annoKey] === undefined) {
throw new Error('Annonation for "' + annoKey + '" of Class "' + clazz.classname + '" does not exist!');
}
if (key.charAt(1) === "_" && key.charAt(2) === "_") {
throw new Error('Cannot annotate private member "' + key.substring(1) + '" of Class "' + clazz.classname);
}
} else {
if (proto[key] !== undefined && key.charAt(0) === "_" && key.charAt(1) === "_") {
throw new Error('Overwriting private member "' + key + '" of Class "' + clazz.classname + '" is not allowed!');
}
if (patch !== true && proto.hasOwnProperty(key)) {
throw new Error('Overwriting member "' + key + '" of Class "' + clazz.classname + '" is not allowed!');
}
}
}
// Annotations are not members
if (key.charAt(0) === '@') {
var annoKey = key.substring(1);
if (members[annoKey] === undefined) {
this.__attachAnno(clazz, "members", annoKey, members[key]);
}
continue;
}
// If it's a property accessor, we need to install it now so that this.base can refer to it
if (proto[key] != undefined && proto[key].$$install) {
proto[key].$$install();
}
// Added helper stuff to functions
// Hint: Could not use typeof function because RegExp objects are functions, too
// Protect to apply base property and aspect support on special attributes e.g.
// classes which are function like as well.
if (base !== false && member instanceof Function && member.$$type == null)
{
if (wrap == true)
{
// wrap "patched" mixin member
member = this.__mixinMemberWrapper(member, proto[key]);
}
else
{
// Configure extend (named base here)
// Hint: proto[key] is not yet overwritten here
if (proto[key]) {
member.base = proto[key];
}
member.self = clazz;
}
if (qx.core.Environment.get("qx.aspects")) {
member = qx.core.Aspect.wrap(clazz.classname + "." + key, member, "member");
}
}
// Attach member
proto[key] = member;
// Attach annotations
this.__attachAnno(clazz, "members", key, members["@" + key]);
}
},
/**
* Wraps a member function of a mixin, which is included using "patch". This
* allows "base" calls in the mixin member function.
*
* @param member {Function} The mixin method to wrap
* @param base {Function} The overwritten method
* @return {Function} the wrapped mixin member
*/
__mixinMemberWrapper : function(member, base)
{
if (base)
{
return function()
{
var oldBase = member.base;
member.base = base;
var retval = member.apply(this, arguments);
member.base = oldBase;
return retval;
};
}
else
{
return member;
}
},
/**
* Add a single interface to a class
*
* @param clazz {Class} class to add interface to
* @param iface {Interface} the Interface to add
*/
__addInterface : function(clazz, iface)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!clazz || !iface) {
throw new Error("Incomplete parameters!");
}
// This differs from mixins, we only check if the interface is already
// directly used by this class. It is allowed however, to have an interface
// included multiple times by extends in the interfaces etc.
if (this.hasOwnInterface(clazz, iface)) {
throw new Error('Interface "' + iface.name + '" is already used by Class "' + clazz.classname + '!');
}
// Check interface and wrap members
if (clazz.$$classtype !== "abstract") {
qx.Interface.assert(clazz, iface, true);
}
}
// Store interface reference
var list = qx.Interface.flatten([iface]);
if (clazz.$$implements)
{
clazz.$$implements.push(iface);
clazz.$$flatImplements.push.apply(clazz.$$flatImplements, list);
}
else
{
clazz.$$implements = [iface];
clazz.$$flatImplements = list;
}
},
/**
* Wrap the constructor of an already existing clazz. This function will
* replace all references to the existing constructor with the new wrapped
* constructor.
*
* @param clazz {Class} The class to wrap
* @return {Class} The wrapped class
*/
__retrospectWrapConstruct : function(clazz)
{
var name = clazz.classname;
var wrapper = this.__wrapConstructor(clazz, name, clazz.$$classtype);
// copy all keys from the wrapped constructor to the wrapper
for (var i=0, a=Object.keys(clazz), l=a.length; i<l; i++)
{
key = a[i];
wrapper[key] = clazz[key];
}
// fix prototype
wrapper.prototype = clazz.prototype;
// fix self references in members
var members = clazz.prototype;
for (var i=0, a=Object.keys(members), l=a.length; i<l; i++)
{
key = a[i];
var method = members[key];
// check if method is available because null values can be stored as
// init values on classes e.g. [BUG #3709]
if (method && method.self == clazz) {
method.self = wrapper;
}
}
// fix base and superclass references in all defined classes
for(var key in th