@ui5/webcomponents-localization
Version:
Localization for UI5 Web Components
483 lines (447 loc) • 16.7 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2024 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides class sap.ui.base.Metadata
import ObjectPath from "../../base/util/ObjectPath.js";
import assert from "../../base/assert.js";
import Log from "../../base/Log.js";
import uniqueSort from "../../base/util/array/uniqueSort.js";
function isFunction(obj) {
return typeof obj === "function";
}
/**
* Creates a new metadata object from the given static infos.
*
* <b>Note:</b> Throughout this class documentation, the described subclass of Object
* is referenced as <i>the described class</i>.
*
* @param {string} sClassName Fully qualified name of the described class
* @param {object} oClassInfo Info to construct the class and its metadata from
* @param {sap.ui.base.Object.MetadataOptions} [oClassInfo.metadata]
* The metadata object describing the class
*
* @class Metadata for a class.
* @author Frank Weigel
* @version 1.120.17
* @since 0.8.6
* @public
* @alias sap.ui.base.Metadata
*/
var Metadata = function (sClassName, oClassInfo) {
assert(typeof sClassName === "string" && sClassName, "Metadata: sClassName must be a non-empty string");
assert(typeof oClassInfo === "object", "Metadata: oClassInfo must be empty or an object");
// support for old usage of Metadata
if (!oClassInfo || typeof oClassInfo.metadata !== "object") {
oClassInfo = {
metadata: oClassInfo || {},
// retrieve class by its name. Using a lookup costs time but avoids the need for redundant arguments to this function
constructor: ObjectPath.get(sClassName) // legacy-relevant, code path not used by extend call
};
oClassInfo.metadata.__version = 1.0;
}
oClassInfo.metadata.__version = oClassInfo.metadata.__version || 2.0;
if (!isFunction(oClassInfo.constructor)) {
throw Error("constructor for class " + sClassName + " must have been declared before creating metadata for it");
}
// invariant: oClassInfo exists, oClassInfo.metadata exists, oClassInfo.constructor exists
this._sClassName = sClassName;
this._oClass = oClassInfo.constructor;
this.extend(oClassInfo);
};
/**
* @private
* @final
*/
Metadata.prototype.extend = function (oClassInfo) {
this.applySettings(oClassInfo);
this.afterApplySettings();
};
/**
* @private
* @since 1.3.1
*/
Metadata.prototype.applySettings = function (oClassInfo) {
var that = this,
oStaticInfo = oClassInfo.metadata,
oPrototype;
if (oStaticInfo.baseType) {
var oParentClass;
if (isFunction(oStaticInfo.baseType)) {
oParentClass = oStaticInfo.baseType;
if (!isFunction(oParentClass.getMetadata)) {
throw new TypeError("baseType must be a UI5 class with a static getMetadata function");
}
} else {
// lookup base class by its name - same reasoning as above
oParentClass = ObjectPath.get(oStaticInfo.baseType); // legacy-relevant, code path not used by extend call
if (!isFunction(oParentClass)) {
Log.fatal("base class '" + oStaticInfo.baseType + "' does not exist");
}
}
// link metadata with base metadata
if (oParentClass.getMetadata) {
this._oParent = oParentClass.getMetadata();
assert(oParentClass === oParentClass.getMetadata().getClass(), "Metadata: oParentClass must match the class in the parent metadata");
} else {
// fallback, if base class has no metadata - can only happen if baseType is a string
this._oParent = new Metadata(oStaticInfo.baseType, {});
}
} else {
this._oParent = undefined;
}
this._bAbstract = !!oStaticInfo["abstract"];
this._bFinal = !!oStaticInfo["final"];
this._sStereotype = oStaticInfo.stereotype || (this._oParent ? this._oParent._sStereotype : "object");
this._bDeprecated = !!oStaticInfo["deprecated"];
// handle interfaces
this._aInterfaces = oStaticInfo.interfaces || [];
// take over metadata from static info
this._aPublicMethods = oStaticInfo.publicMethods || [];
// interfaces info possibly not unique
this._bInterfacesUnique = false;
// enrich prototype
oPrototype = this._oClass.prototype;
for (var n in oClassInfo) {
if (n !== "metadata" && n !== "constructor") {
oPrototype[n] = oClassInfo[n];
if (!n.match(/^_|^on|^init$|^exit$/)) {
// TODO hard coded knowledge about event handlers ("on") and about init/exit hooks is not nice....
that._aPublicMethods.push(n);
}
}
}
};
/**
* Called after new settings have been applied.
*
* Typically, this method is used to do some cleanup (e.g. uniqueness)
* or to calculate an optimized version of some data.
* @private
* @since 1.3.1
*/
Metadata.prototype.afterApplySettings = function () {
// create the flattened "all" view
if (this._oParent) {
this._aAllPublicMethods = this._oParent._aAllPublicMethods.concat(this._aPublicMethods);
this._bInterfacesUnique = false;
} else {
this._aAllPublicMethods = this._aPublicMethods;
}
};
/**
* Stereotype of the described class.
*
* @experimental might be enhanced to a set of stereotypes
* @private
* @ui5-restricted
*/
Metadata.prototype.getStereotype = function () {
return this._sStereotype;
};
/**
* Returns the fully qualified name of the described class
* @return {string} name of the described class
* @public
*/
Metadata.prototype.getName = function () {
return this._sClassName;
};
/**
* Returns the (constructor of the) described class
* @return {function(new:sap.ui.base.Object)} class described by this metadata
* @public
*/
Metadata.prototype.getClass = function () {
return this._oClass;
};
/**
* Returns the metadata object of the base class of the described class
* or undefined if the class has no (documented) base class.
*
* @return {sap.ui.base.Metadata | undefined} metadata of the base class
* @public
*/
Metadata.prototype.getParent = function () {
return this._oParent;
};
/**
* Removes duplicate names in place from the interfaces and public methods members of this metadata object.
*
* @private
*/
Metadata.prototype._dedupInterfaces = function () {
if (!this._bInterfacesUnique) {
uniqueSort(this._aInterfaces);
uniqueSort(this._aPublicMethods);
uniqueSort(this._aAllPublicMethods);
this._bInterfacesUnique = true;
}
};
/**
* Returns an array with the names of the public methods declared by the described class, methods of
* ancestors are not listed.
*
* @return {string[]} array with names of public methods declared by the described class
* @deprecated As of 1.58, this method should not be used for productive code. The accuracy of the returned
* information highly depends on the concrete class and is not actively monitored. There might be
* more public methods or some of the returned methods might not really be intended for public use.
* In general, pure visibility information should not be exposed in runtime metadata but be part of the
* documentation.
* Subclasses of <code>sap.ui.base.Object</code> might decide to provide runtime metadata describing
* their public API, but this then should not be backed by this method.
* See {@link sap.ui.core.mvc.ControllerMetadata#getAllMethods} for an example.
* @public
*/
Metadata.prototype.getPublicMethods = function () {
this._dedupInterfaces();
return this._aPublicMethods;
};
/**
* Returns an array with the names of all public methods declared by the described class
* and all its ancestors classes.
*
* @return {string[]} array with names of all public methods provided by the described class and its ancestors
* @deprecated As of 1.58, this method should not be used for productive code. The accuracy of the returned
* information highly depends on the concrete class and is not actively monitored. There might be
* more public methods or some of the returned methods might not really be intended for public use.
* In general, pure visibility information should not be exposed in runtime metadata but be part of the
* documentation.
* Subclasses of <code>sap.ui.base.Object</code> might decide to provide runtime metadata describing
* their public API, but this then should not be backed by this method.
* See {@link sap.ui.core.mvc.ControllerMetadata#getAllMethods} for an example.
* @public
*/
Metadata.prototype.getAllPublicMethods = function () {
this._dedupInterfaces();
return this._aAllPublicMethods;
};
/**
* Returns the names of interfaces implemented by the described class.
* As the representation of interfaces is not clear yet, this method is still private.
*
* @return {string} array of names of implemented interfaces
* @private
*/
Metadata.prototype.getInterfaces = function () {
this._dedupInterfaces();
return this._aInterfaces;
};
/**
* Checks whether the described class or one of its ancestor classes implements the given interface.
*
* @param {string} sInterface name of the interface to test for (in dot notation)
* @return {boolean} whether this class implements the interface
* @public
*/
Metadata.prototype.isInstanceOf = function (sInterface) {
if (this._oParent) {
if (this._oParent.isInstanceOf(sInterface)) {
return true;
}
}
var a = this._aInterfaces;
for (var i = 0, l = a.length; i < l; i++) {
// FIXME doesn't handle interface inheritance (requires object representation for interfaces)
if (a[i] === sInterface) {
return true;
}
}
return false;
};
/*
* Lazy calculation of the set of implemented types.
*
* A calculation function is configured as getter for the <code>_mImplementedTypes</code>
* on the prototype object. On first call for a metadata instance, it collects
* the implemented types (classes, interfaces) from the described class and
* any base classes and writes it to the property <code>_mImplementedTypes</code> of the
* current instance of metadata. Future read access to the property will immediately
* return the instance property and not call the calculation function again.
*/
Object.defineProperty(Metadata.prototype, "_mImplementedTypes", {
get: function () {
if (this === Metadata.prototype) {
throw new Error("sap.ui.base.Metadata: The '_mImplementedTypes' property must not be accessed on the prototype");
}
// create map of types, including inherited types
// Note: to save processing time and memory, the inherited types are merged via the prototype chain of 'result'
var result = Object.create(this._oParent ? this._oParent._mImplementedTypes : null);
/*
* Flat alternative:
* var result = Object.create(null);
* if ( this._oParent ) {
* Object.assign(result, this._oParent._mImplementedTypes);
* }
*/
// add own class
result[this._sClassName] = true;
// additionally collect interfaces
var aInterfaces = this._aInterfaces,
i = aInterfaces.length;
while (i-- > 0) {
if (!result[aInterfaces[i]]) {
// take care to write property only if it hasn't been set already
result[aInterfaces[i]] = true;
}
}
// write instance property, hiding the getter on the prototype
Object.defineProperty(this, "_mImplementedTypes", {
value: Object.freeze(result),
writable: false,
configurable: false
});
return result;
},
configurable: true
});
/**
* Checks whether the class described by this metadata object is of the named type.
*
* This check is solely based on the type names as declared in the class metadata.
* It compares the given <code>vTypeName</code> with the name of this class, with the
* names of any base class of this class and with the names of all interfaces
* implemented by any of the aforementioned classes.
*
* Instead of a single type name, an array of type names can be given and the method
* will check if this class is of any of the listed types (logical or).
*
* Should the UI5 class system in future implement additional means of associating classes
* with type names (e.g. by introducing mixins), then this method might detect matches
* for those names as well.
*
* @param {string|string[]} vTypeName Type or types to check for
* @returns {boolean} Whether this class is of the given type or of any of the given types
* @public
* @since 1.56
*/
Metadata.prototype.isA = function (vTypeName) {
var mTypes = this._mImplementedTypes;
if (Array.isArray(vTypeName)) {
for (var i = 0; i < vTypeName.length; i++) {
if (vTypeName[i] in mTypes) {
return true;
}
}
return false;
}
// Note: the check with 'in' also finds inherited types via the prototype chain of mTypes
return vTypeName in mTypes;
};
/**
* Returns whether the described class is abstract
* @return {boolean} whether the class is abstract
* @public
*/
Metadata.prototype.isAbstract = function () {
return this._bAbstract;
};
/**
* Returns whether the described class is final
* @return {boolean} whether the class is final
* @public
*/
Metadata.prototype.isFinal = function () {
return this._bFinal;
};
/**
* Whether the described class is deprecated and should not be used any more
*
* @return {boolean} whether the class is considered deprecated
* @public
* @since 1.26.4
*/
Metadata.prototype.isDeprecated = function () {
return this._bDeprecated;
};
/**
* Adds one or more new methods to the list of API methods.
*
* Can be used by contributer classes (like the EnabledPropagator) to enrich the declared set of methods.
* The method can either be called with multiple names (strings) or with one array of strings.
*
* <b>Note</b>: the newly added method(s) will only be visible in {@link sap.ui.base.Interface interface}
* objects that are created <i>after</i> this method has been called.
*
* @param {string|string[]} sMethod name(s) of the new method(s)
*/
Metadata.prototype.addPublicMethods = function (sMethod /* ... */) {
var aNames = sMethod instanceof Array ? sMethod : arguments;
Array.prototype.push.apply(this._aPublicMethods, aNames);
Array.prototype.push.apply(this._aAllPublicMethods, aNames);
this._bInterfacesUnique = false;
};
/**
* @since 1.3.1
* @private
*/
Metadata.createClass = function (fnBaseClass, sClassName, oClassInfo, FNMetaImpl) {
if (typeof fnBaseClass === "string") {
FNMetaImpl = oClassInfo;
oClassInfo = sClassName;
sClassName = fnBaseClass;
fnBaseClass = null;
}
assert(!fnBaseClass || isFunction(fnBaseClass));
assert(typeof sClassName === "string" && !!sClassName);
assert(!oClassInfo || typeof oClassInfo === "object");
assert(!FNMetaImpl || isFunction(FNMetaImpl));
// allow metadata class to preprocess
FNMetaImpl = FNMetaImpl || Metadata;
if (isFunction(FNMetaImpl.preprocessClassInfo)) {
oClassInfo = FNMetaImpl.preprocessClassInfo(oClassInfo);
}
// normalize oClassInfo
oClassInfo = oClassInfo || {};
oClassInfo.metadata = oClassInfo.metadata || {};
if (!oClassInfo.hasOwnProperty('constructor')) {
oClassInfo.constructor = undefined;
}
var fnClass = oClassInfo.constructor;
assert(!fnClass || isFunction(fnClass));
// ensure defaults
if (fnBaseClass) {
// default constructor just delegates to base class
if (!fnClass) {
if (oClassInfo.metadata.deprecated) {
// create default factory with deprecation warning
fnClass = function () {
Log.warning("Usage of deprecated class: " + sClassName);
fnBaseClass.apply(this, arguments);
};
} else {
// create default factory
fnClass = function () {
fnBaseClass.apply(this, arguments);
};
}
}
// create prototype chain
fnClass.prototype = Object.create(fnBaseClass.prototype);
fnClass.prototype.constructor = fnClass;
// enforce correct baseType
oClassInfo.metadata.baseType = fnBaseClass;
} else {
// default constructor does nothing
fnClass = fnClass || function () {};
// enforce correct baseType
delete oClassInfo.metadata.baseType;
}
oClassInfo.constructor = fnClass;
// make the class visible as JS Object
ObjectPath.set(sClassName, fnClass);
// add metadata
var oMetadata = new FNMetaImpl(sClassName, oClassInfo);
fnClass.getMetadata = fnClass.prototype.getMetadata = function () {
return oMetadata;
};
// enrich function
if (!fnClass.getMetadata().isFinal()) {
fnClass.extend = function (sSCName, oSCClassInfo, fnSCMetaImpl) {
return Metadata.createClass(fnClass, sSCName, oSCClassInfo, fnSCMetaImpl || FNMetaImpl);
};
}
return fnClass;
};
export default Metadata;