UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

655 lines (568 loc) 20 kB
/* ************************************************************************ 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) ************************************************************************ */ /** * This class is used to define interfaces (similar to Java interfaces). * * See the description of the {@link #define} method how an interface is * defined. * * @require(qx.lang.normalize.Array) */ qx.Bootstrap.define("qx.Interface", { statics : { /* --------------------------------------------------------------------------- PUBLIC API --------------------------------------------------------------------------- */ /** * Define a new interface. Interface definitions look much like class definitions. * * The main difference is that the bodies of functions defined in <code>members</code> * and <code>statics</code> are called before the original function with the * same arguments. This can be used to check the passed arguments. If the * checks fail, an exception should be thrown. It is convenient to use the * method defined in {@link qx.core.MAssert} to check the arguments. * * In the <code>build</code> version the checks are omitted. * * For properties only the names are required so the value of the properties * can be empty maps. * * Example: * <pre class='javascript'> * qx.Interface.define("name", * { * extend: [SuperInterfaces], * * statics: * { * PI : 3.14 * }, * * properties: {"color": {}, "name": {} }, * * members: * { * meth1: function() {}, * meth2: function(a, b) { this.assertArgumentsCount(arguments, 2, 2); }, * meth3: function(c) { this.assertInterface(c.constructor, qx.some.Interface); } * }, * * events : * { * keydown : "qx.event.type.KeySequence" * } * }); * </pre> * * @param name {String} name of the interface * @param config {Map ? null} Interface definition structure. The configuration map has the following keys: * <table> * <tr><th>Name</th><th>Type</th><th>Description</th></tr> * <tr><th>extend</th><td>Interface |<br>Interface[]</td><td>Single interface or array of interfaces this interface inherits from.</td></tr> * <tr><th>members</th><td>Map</td><td>Map of members of the interface.</td></tr> * <tr><th>statics</th><td>Map</td><td> * Map of statics of the interface. The statics will not get copied into the target class. * This is the same behaviour as statics in mixins ({@link qx.Mixin#define}). * </td></tr> * <tr><th>properties</th><td>Map</td><td>Map of properties and their definitions.</td></tr> * <tr><th>events</th><td>Map</td><td>Map of event names and the corresponding event class name.</td></tr> * </table> * * @return {qx.Interface} The configured interface */ define : function(name, config) { if (config) { // Normalize include if (config.extend && !(qx.Bootstrap.getClass(config.extend) === "Array")) { config.extend = [config.extend]; } // Validate incoming data if (qx.core.Environment.get("qx.debug")) { this.__validateConfig(name, config); } // Create interface from statics var iface = config.statics ? config.statics : {}; // Attach configuration if (config.extend) { iface.$$extends = config.extend; } if (config.properties) { iface.$$properties = config.properties; } if (config.members) { iface.$$members = config.members; } if (config.events) { iface.$$events = config.events; } } else { // Create empty interface var iface = {}; } // Add Basics iface.$$type = "Interface"; iface.name = name; // Attach toString iface.toString = this.genericToString; // Assign to namespace iface.basename = qx.Bootstrap.createNamespace(name, iface); // Add to registry qx.Interface.$$registry[name] = iface; // Return final interface return iface; }, /** * Returns an interface by name * * @param name {String} class name to resolve * @return {Class} the class */ getByName : function(name) { return this.$$registry[name]; }, /** * Determine if interface exists * * @param name {String} Interface name to check * @return {Boolean} true if interface exists */ isDefined : function(name) { return this.getByName(name) !== undefined; }, /** * Determine the number of interfaces which are defined * * @return {Number} the number of interfaces */ getTotalNumber : function() { return qx.Bootstrap.objectGetLength(this.$$registry); }, /** * Generates a list of all interfaces including their super interfaces * (resolved recursively) * * @param ifaces {Interface[] ? []} List of interfaces to be resolved * @return {Array} List of all interfaces */ flatten : function(ifaces) { if (!ifaces) { return []; } // we need to create a copy and not to modify the existing array var list = ifaces.concat(); for (var i=0, l=ifaces.length; i<l; i++) { if (ifaces[i].$$extends) { list.push.apply(list, this.flatten(ifaces[i].$$extends)); } } return list; }, /** * Assert members * * @param object {qx.core.Object} The object, which contains the methods * @param clazz {Class} class of the object * @param iface {Interface} the interface to verify * @param wrap {Boolean ? false} wrap functions required by interface to * check parameters etc. * @param shouldThrow {Boolean} if <code>false</code>, the method * will return a boolean instead of throwing an exception * @return {Boolean} <code>true</code> if all members are supported */ __checkMembers : function(object, clazz, iface, wrap, shouldThrow) { // Validate members var members = iface.$$members; if (members) { for (var key in members) { if (qx.Bootstrap.isFunction(members[key])) { var isPropertyMethod = this.__isPropertyMethod(clazz, key); var hasMemberFunction = isPropertyMethod || qx.Bootstrap.isFunction(object[key]); if (!hasMemberFunction) { if (shouldThrow) { throw new Error( 'Implementation of method "' + key + '" is missing in class "' + clazz.classname + '" required by interface "' + iface.name + '"' ); } else { return false; } } // Only wrap members if the interface was not been applied yet. This // can easily be checked by the recursive hasInterface method. var shouldWrapFunction = wrap === true && !isPropertyMethod && !qx.util.OOUtil.hasInterface(clazz, iface); if (shouldWrapFunction) { object[key] = this.__wrapInterfaceMember( iface, object[key], key, members[key] ); } } else { // Other members are not checked more detailed because of // JavaScript's loose type handling if (typeof object[key] === undefined) { if (typeof object[key] !== "function") { if (shouldThrow) { throw new Error( 'Implementation of member "' + key + '" is missing in class "' + clazz.classname + '" required by interface "' + iface.name + '"' ); } else { return false; } } } } } } if (!shouldThrow) { return true; } }, /** * Internal helper to detect if the method will be generated by the * property system. * * @param clazz {Class} The current class. * @param methodName {String} The name of the method. * * @return {Boolean} true, if the method will be generated by the property * system. */ __isPropertyMethod: function(clazz, methodName) { var match = methodName.match(/^(is|toggle|get|set|reset)(.*)$/); if (!match) { return false; } var propertyName = qx.Bootstrap.firstLow(match[2]); var isPropertyMethod = qx.util.OOUtil.getPropertyDefinition(clazz, propertyName); if (!isPropertyMethod) { return false; } var isBoolean = match[0] === "is" || match[0] === "toggle"; if (isBoolean) { return qx.util.OOUtil.getPropertyDefinition(clazz, propertyName).check === "Boolean"; } return true; }, /** * Assert properties * * @param clazz {Class} class to check interface for * @param iface {Interface} the interface to verify * @param shouldThrow {Boolean} if <code>false</code>, the method * will return a boolean instead of throwing an exception * @return {Boolean} <code>true</code> if all properties are supported */ __checkProperties : function(clazz, iface, shouldThrow) { if (iface.$$properties) { for (var key in iface.$$properties) { if (!qx.util.OOUtil.getPropertyDefinition(clazz, key)) { if (shouldThrow) { throw new Error( 'The property "' + key + '" is not supported by Class "' + clazz.classname + '"!' ); } else { return false; } } } } if (!shouldThrow) { return true; } }, /** * Assert events * * @param clazz {Class} class to check interface for * @param iface {Interface} the interface to verify * @param shouldThrow {Boolean} if <code>false</code>, the method * will return a boolean instead of throwing an exception * @return {Boolean} <code>true</code> if all events are supported */ __checkEvents : function(clazz, iface, shouldThrow) { if (iface.$$events) { for (var key in iface.$$events) { if (!qx.util.OOUtil.supportsEvent(clazz, key)) { if (shouldThrow) { throw new Error( 'The event "' + key + '" is not supported by Class "' + clazz.classname + '"!' ); } else { return false; } } } } if (!shouldThrow) { return true; } }, /** * Asserts that the given object implements all the methods defined in the * interface. This method throws an exception if the object does not * implement the interface. * * @param object {qx.core.Object} Object to check interface for * @param iface {Interface} The interface to verify */ assertObject : function(object, iface) { var clazz = object.constructor; this.__checkMembers(object, clazz, iface, false, true); this.__checkProperties(clazz, iface, true); this.__checkEvents(clazz, iface, true); // Validate extends, recursive var extend = iface.$$extends; if (extend) { for (var i=0, l=extend.length; i<l; i++) { this.assertObject(object, extend[i]); } } }, /** * Checks if an interface is implemented by a class * * @param clazz {Class} class to check interface for * @param iface {Interface} the interface to verify * @param wrap {Boolean ? false} wrap functions required by interface to * check parameters etc. */ assert : function(clazz, iface, wrap) { this.__checkMembers(clazz.prototype, clazz, iface, wrap, true); this.__checkProperties(clazz, iface, true); this.__checkEvents(clazz, iface, true); // Validate extends, recursive var extend = iface.$$extends; if (extend) { for (var i=0, l=extend.length; i<l; i++) { this.assert(clazz, extend[i], wrap); } } }, /** * Asserts that the given object implements all the methods defined in the * interface. * * @param object {qx.core.Object} Object to check interface for * @param iface {Interface} The interface to verify * @return {Boolean} <code>true</code> if the objects implements the interface */ objectImplements : function(object, iface) { var clazz = object.constructor; if (!this.__checkMembers(object, clazz, iface) || !this.__checkProperties(clazz, iface) || !this.__checkEvents(clazz, iface)) { return false; } // Validate extends, recursive var extend = iface.$$extends; if (extend) { for (var i=0, l=extend.length; i<l; i++) { if (!this.objectImplements(object, extend[i])) { return false; } } } return true; }, /** * Tests whether an interface is implemented by a class, without throwing an * exception when it doesn't. * * @param clazz {Class} class to check interface for * @param iface {Interface} the interface to verify * @return {Boolean} <code>true</code> if interface is implemented */ classImplements : function(clazz, iface) { if (!this.__checkMembers(clazz.prototype, clazz, iface) || !this.__checkProperties(clazz, iface) || !this.__checkEvents(clazz, iface)) { return false; } // Validate extends, recursive var extend = iface.$$extends; if (extend) { for (var i=0, l=extend.length; i<l; i++) { if (!this.has(clazz, extend[i])) { return false; } } } return true; }, /* --------------------------------------------------------------------------- PRIVATE/INTERNAL API --------------------------------------------------------------------------- */ /** * This method will be attached to all interface to return * a nice identifier for them. * * @internal * @return {String} The interface identifier */ genericToString : function() { return "[Interface " + this.name + "]"; }, /** Registry of all defined interfaces */ $$registry : {}, /** * Wrap a method with a precondition check. * * @signature function(iface, origFunction, functionName, preCondition) * @param iface {String} Name of the interface, where the pre condition * was defined. (Used in error messages). * @param origFunction {Function} function to wrap. * @param functionName {String} name of the function. (Used in error messages). * @param preCondition {Function}. This function gets called with the arguments of the * original function. If this function return true the original function is called. * Otherwise an exception is thrown. * @return {Function} wrapped function */ __wrapInterfaceMember : qx.core.Environment.select("qx.debug", { "true": function(iface, origFunction, functionName, preCondition) { function wrappedFunction() { // call precondition preCondition.apply(this, arguments); // call original function return origFunction.apply(this, arguments); } origFunction.wrapper = wrappedFunction; return wrappedFunction; }, "default" : function(iface, origFunction, functionName, preCondition) {} }), /** @type {Map} allowed keys in interface definition */ __allowedKeys : qx.core.Environment.select("qx.debug", { "true": { "extend" : "object", // Interface | Interface[] "statics" : "object", // Map "members" : "object", // Map "properties" : "object", // Map "events" : "object" // Map }, "default" : null }), /** * Validates incoming configuration and checks 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) { if (qx.core.Environment.get("qx.debug")) { // Validate keys var allowed = this.__allowedKeys; for (var key in config) { if (allowed[key] === undefined) { throw new Error('The configuration key "' + key + '" in class "' + name + '" is not allowed!'); } if (config[key] == null) { throw new Error("Invalid key '" + key + "' in interface '" + name + "'! The value is undefined/null!"); } if (allowed[key] !== null && typeof config[key] !== allowed[key]) { throw new Error('Invalid type of key "' + key + '" in interface "' + name + '"! The type of the key must be "' + allowed[key] + '"!'); } } // Validate maps var maps = [ "statics", "members", "properties", "events" ]; for (var i=0, l=maps.length; i<l; i++) { var key = maps[i]; if (config[key] !== undefined && ([ "Array", "RegExp", "Date" ].indexOf(qx.Bootstrap.getClass(config[key])) != -1 || config[key].classname !== undefined)) { throw new Error('Invalid key "' + key + '" in interface "' + name + '"! The value needs to be a map!'); } } // Validate extends if (config.extend) { for (var i=0, a=config.extend, l=a.length; i<l; i++) { if (a[i] == null) { throw new Error("Extends of interfaces must be interfaces. The extend number '" + i+1 + "' in interface '" + name + "' is undefined/null!"); } if (a[i].$$type !== "Interface") { throw new Error("Extends of interfaces must be interfaces. The extend number '" + i+1 + "' in interface '" + name + "' is not an interface!"); } } } // Validate statics if (config.statics) { for (var key in config.statics) { if (key.toUpperCase() !== key) { throw new Error('Invalid key "' + key + '" in interface "' + name + '"! Static constants must be all uppercase.'); } switch(typeof config.statics[key]) { case "boolean": case "string": case "number": break; default: throw new Error('Invalid key "' + key + '" in interface "' + name + '"! Static constants must be all of a primitive type.'); } } } } }, "default" : function(name, config) {} }) } });