/**
* @copyright 2016 Tridium, Inc. All Rights Reserved.
*/
/* jshint browser: true */
/**
* @module nmodule/webEditors/rc/wb/mgr/MgrTypeInfo
*/
define([ 'baja!',
'Promise',
'underscore',
'nmodule/webEditors/rc/fe/baja/util/typeUtils' ], function (
baja,
Promise,
_,
typeUtils) {
'use strict';
var isComponent = typeUtils.isComponent,
getTypeDisplayName = typeUtils.getTypeDisplayName;
////////////////////////////////////////////////////////////////
// Support
////////////////////////////////////////////////////////////////
/**
* Test whether the given parameter is an object with a type spec property.
*
* @param {Object} o
* @returns {boolean} true if a type spec can be obtained by reading a string property.
*/
function hasTypeSpecString(o) {
return (o.hasOwnProperty('typeSpec')) && (typeof o.typeSpec === 'string');
}
/**
* Test whether the given parameter is an object with a `getTypeSpec()` function.
*
* @param {Object} o
* @returns {boolean} true if a type spec can be obtained by calling the getTypeSpec() function.
*/
function hasTypeSpecFunction(o) {
return o && (typeof o === 'object') && ('getTypeSpec' in o) && (typeof o.getTypeSpec === 'function');
}
////////////////////////////////////////////////////////////////
// TypeImpl
////////////////////////////////////////////////////////////////
/**
* TypeImpl provides an implementation of the MgrTypeInfo functionality
* based on a BajaScript `Type` instance. This constructor is used when
* the MgrTypeInfo is being created from a type spec string, a `Type`
* instance, or an agent info.
*
* @inner
* @private
* @class
*/
var TypeImpl = function TypeImpl(type, displayName, duplicate) {
this.$type = type;
this.$displayName = displayName;
this.$duplicate = !!duplicate;
};
/**
* TypeImpl provides a different implementation of getDisplayName(), where
* the module name will be appended to the resulting string in the case where
* the type name is a duplicate.
*
* @returns {string}
*/
TypeImpl.prototype.getDisplayName = function () {
var that = this,
str = that.$displayName;
if (that.$duplicate) { str = str + ' (' + that.$type.getModuleName() + ')'; }
return str;
};
/**
* Returns the icon configured for a type, based on the module's lexicon.
* @returns {baja.Icon} The icon configured for the type.
*/
TypeImpl.prototype.getIcon = function () {
return this.$type.getIcon();
};
/**
* Returns a Promise to create a new instance of the type represented by this instance.
* @returns {Promise.<baja.Component>}
*/
TypeImpl.prototype.newInstance = function () {
return Promise.resolve(baja.$(this.$type));
};
/**
* Test whether the wrapped type can be matched against the given database component's type.
*/
TypeImpl.prototype.isMatchable = function (db) {
return db && db.getType().is(this.$type);
};
/**
* Return the string used for comparison against another MgrTypeInfo.
* @returns {String}
*/
TypeImpl.prototype.getCompareString = function () {
return this.$type.toString();
};
/**
* Get the underlying type wrapped by this instance.
* @returns {baja.Type}
*/
TypeImpl.prototype.getType = function () {
return this.$type;
};
/**
* Mark or unmark this instance as a duplicate, meaning that we have
* two or more types with the same type name in different modules. If
* the parameter is true, the string returned by `getDisplayName()`
* will have the module name appended to make it unique.
*
* @param {Boolean} duplicate
*/
TypeImpl.prototype.$setDuplicate = function (duplicate) {
this.$duplicate = !!duplicate;
};
////////////////////////////////////////////////////////////////
// PrototypeImpl
////////////////////////////////////////////////////////////////
/**
* TypeImpl provides an implementation of the MgrTypeInfo functionality
* based on an existing prototype BComponent.
*
* @inner
* @private
* @class
*/
var PrototypeImpl = function PrototypeImpl(proto, displayName) {
this.$proto = proto;
this.$displayName = displayName;
};
PrototypeImpl.prototype.constructor = PrototypeImpl;
/**
* Get the display name from the wrapped prototype component's type.
* @returns {string}
*/
PrototypeImpl.prototype.getDisplayName = function () {
return this.$displayName;
};
/**
* Return the icon obtained from the wrapped prototype component.
* @returns {baja.Icon}
*/
PrototypeImpl.prototype.getIcon = function () {
return this.$proto.getIcon();
};
/**
* Return a new `Component` instance, cloned as an exact copy
* of the prototype.
*
* @returns {Promise.<baja.Component>}
*/
PrototypeImpl.prototype.newInstance = function () {
return Promise.resolve(this.$proto.newCopy(true));
};
/**
* Test whether the prototype's type can be matched against the given database component's type.
*/
PrototypeImpl.prototype.isMatchable = function (db) {
return db.getType().is(this.$proto.getType());
};
/**
* Return the string used for comparison against another MgrTypeInfo.
* @returns {string}
*/
PrototypeImpl.prototype.getCompareString = function () {
return this.$proto.getType().toString();
};
/**
* Return the type of the underlying component prototype.
* @returns {*}
*/
PrototypeImpl.prototype.getType = function () {
return this.$proto.getType();
};
////////////////////////////////////////////////////////////////
// MgrTypeInfo
////////////////////////////////////////////////////////////////
/**
* API Status: **Development**
*
* MgrTypeInfo wraps information about what type to create
* in the station database when doing a new or add operation.
* This information may come from an exact type, a registry
* query for concrete types given a base type, or from a component
* instance used as a prototype.
*
* In addition to the basic type operations, this provides extra
* functionality specific to the manager views, such as the ability
* to compare and match types.
*
* This constructor should be not called directly. Client code should
* use the static `.make()` function.
*
* @class
* @alias module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo
*/
var MgrTypeInfo = function MgrTypeInfo(impl) {
this.$impl = impl;
};
/**
* Static function to compare an array of MgrTypeInfos for duplicate type names.
* This is used to alter the display name for any non-prototype created MgrTypeInfos,
* where there may be two or more modules exposing types with the same display names.
*
* @static
* @param {Array.<module:nmodule/webEditors/rc/wb/mgr/MgrTypeInfo>} infos
*/
MgrTypeInfo.markDuplicates = function (infos) {
if (!infos || infos.length === 0) { return; }
var names = infos.map(function (inf) { return inf.getType().getTypeName(); }),
counts = _.countBy(names, _.identity);
_.each(infos, function (inf, i) {
if ((counts[names[i]] > 1) && (inf.$impl instanceof TypeImpl)) {
inf.$impl.$setDuplicate(true);
}
});
};
/**
* Static `.make()` function, returning a `Promise` that will resolve
* to a single `MgrTypeInfo` or array of `MgrTypeInfo`s, depending on the
* arguments passed to the function. This is the normal way to obtain a
* MgrTypeInfo instance; the type's constructor should not be used by client
* code directly.
*
* @example
* <caption>
* Make an array of MgrTypeInfos from an array of type spec strings.
* </caption>
* MgrTypeInfo.make({
* from: [ 'baja:AbsTime', 'baja:Date', 'baja.RelTime' ]
* })
* .then(function (mgrInfos) {
* // ... do something with the array of MgrTypeInfo
* });
*
* @example
* <caption>
* Get the concrete MgrTypeInfos for the abstract ControlPoint type.
* </caption>
* MgrTypeInfo.make({
* from: 'baja:ControlPoint',
* concreteTypes: true
* })
* .then(function (mgrInfos) {
* // ... do something with the array of MgrTypeInfo
* });
*
* @example
* <caption>
* Get the MgrTypeInfos for the agents on a type
* </caption>
* baja.registry.getAgents("type:moduleName:TypeName")
* .then(function (agentInfos) {
* return MgrTypeInfo.make({
* from: agentInfos
* });
* })
* .then(function (mgrInfos) {
* // ... do something with the array of MgrTypeInfo
* });
* @static
*
* @param {Object} params an Object containing the function's arguments
*
* @param {String|baja.Type|Array} params.from The type spec used to create the
* type information. This may be a single type spec string, or a BajaScript `Type` instance,
* or may be an array of spec strings or `Types`.
*
* @param {Boolean} [params.concreteTypes] true if the `from` parameter should be used
* as a base type to create an Array of `MgrTypeInfo`s for its concrete types. If this parameter
* is specified as true, the `from` parameter must contain a single base type.
*
* @param {baja.comm.Batch} [params.batch] An optional batch object, which if provided can be
* used to batch network calls.
*
* @returns {Promise} Either a single `MgrTypeInfo` instance, or an array of MgrTypeInfos,
* depending on the value passed in the 'from' parameter and the value of the 'concreteTypes'
* parameter. If the 'from' parameter specifies a single type and 'concreteTypes' is either
* false or undefined, the returned value will be a single `MgrTypeInfo`, otherwise the returned
* value will be an array of `MgrTypeInfo`s.
*/
MgrTypeInfo.make = function (params) {
params = baja.objectify(params, 'from');
if (!params.from) {
throw new Error('MgrTypeInfo.make() requires a source type spec, agent or component.');
}
return makePromise(params);
};
MgrTypeInfo.prototype.constructor = MgrTypeInfo;
/**
* Get the display name of the type to create.
* @returns {String}
*/
MgrTypeInfo.prototype.getDisplayName = function () {
return this.$impl.getDisplayName.apply(this.$impl, arguments);
};
/**
* Get the BajaScript Type to be created by this MgrTypeInfo instance.
* @returns {baja.Type}
*/
MgrTypeInfo.prototype.getType = function () {
return this.$impl.getType();
};
/**
* Get the icon of the type to create.
* @returns {baja.Icon}
*/
MgrTypeInfo.prototype.getIcon = function () {
return this.$impl.getIcon();
};
/**
* Get the type as a slot name. The default implementation
* returns the display name stripped of spaces and escaped.
*
* @returns {String}
*/
MgrTypeInfo.prototype.toSlotName = function () {
return baja.SlotPath.escape(this.getDisplayName().replace(/ /g, ''));
};
/**
* Returns a Promise that will create a new instance of a component
* from the type information.
*
* @returns {Promise.<baja.Component>}
*/
MgrTypeInfo.prototype.newInstance = function () {
return this.$impl.newInstance();
};
/**
* Return true if this type may be used to perform a learn match against the
* specified database component.
*
* @param {baja.Component} db the database component
* @returns {Boolean}
*/
MgrTypeInfo.prototype.isMatchable = function (db) {
return this.$impl.isMatchable(db);
};
/**
* Equality comparison for two MgrTypeInfo instances based on a comparison
* of the display name. This will return false if the other object is not a
* MgrTypeInfo instance.
*
* @param {Object} other the object to be compared against
* @returns {Boolean}
*/
MgrTypeInfo.prototype.equals = function (other) {
if (other && typeof other === 'object' && other instanceof MgrTypeInfo) {
return this.$impl.getCompareString() === other.$impl.getCompareString();
}
return false;
};
/**
* Returns the display name for the type represented by this instance.
* @returns {String}
*/
MgrTypeInfo.prototype.toString = function () {
return this.getDisplayName();
};
/**
* Helper function to be passed to an array sorting function to ensure MgrTypeInfo instances are
* ordered according to the display name.
*
* @example
* <caption>
* Sort the array of MgrTypeInfos obtained from the make() function.
* </caption>
* MgrTypeInfo.make({
* from: 'baja:ControlPoint',
* concreteTypes: true
* })
* .then(function (mgrInfos) {
* mgrInfos.sort(MgrTypeInfo.BY_DISPLAY_NAME);
* });
*/
MgrTypeInfo.BY_DISPLAY_NAME = function (a, b) {
var nameA = a.getDisplayName(),
nameB = b.getDisplayName();
if (nameA < nameB) { return -1; }
if (nameA > nameB) { return 1; }
return 0;
};
/**
* Make a new MgrTypeInfo instance from the given component prototype.
* This creates an implementation instance that wraps the given component.
*
* @param {baja.Component} proto the prototype component
* @returns {Promise}
*/
function makeFromPrototype(proto) {
return getTypeDisplayName(proto.getType())
.then(function (displayName) {
return new MgrTypeInfo(new PrototypeImpl(proto, displayName));
});
}
/**
* Make a new MgrTypeInfo from a BajaScript `Type` or an agent info object obtained
* from the registry.
*
* @param type {baja.Type|Object} Either a loaded BajaScript `Type` or an AgentInfo object.
* @returns {Promise}
*/
function makeWithTypeImplementation(type) {
return getTypeDisplayName(type)
.then(function (displayName) {
return new MgrTypeInfo(new TypeImpl(type, displayName));
});
}
/**
* Create and return the promise that will resolve to the MgrTypeInfo instance(s).
*
* @param {Object} params the parameter object passed to the make() function.
* @returns {Promise}
*/
function makePromise(params) {
if ((_.isArray(params.from) && (params.from.length === 0))) {
return Promise.resolve([]);
} else if (isComponent(params.from)) {
return makeFromPrototype(params.from);
} else if (params.concreteTypes) {
if (_.isArray(params.from)) {
throw new Error('Must specify a single base type when getting concrete types.');
}
return getFromBaseType(params.from, params.batch);
} else if (_.isArray(params.from)) {
return getFromTypeSpecs(params.from, params.batch);
} else {
return getFromTypeSpecs([ params.from ], params.batch).then(_.first);
}
}
/**
* Make a new MgrTypeInfo instance from the given type. If the typespec
* is not already loaded, this will try to import the types into the
* registry.
*
* @param from
* @param {baja.comm.Batch} batch An optional batch instance, used to combine network calls.
* @returns {Promise}
*/
function getFromTypeSpecs(from, batch) {
function getSpecString(o) {
if (hasTypeSpecFunction(o)) {
return o.getTypeSpec();
} else if (hasTypeSpecString(o)) {
return o.typeSpec;
} else {
return String(o);
}
}
var typeSpecs = _.map(from, getSpecString),
importArgs = { typeSpecs: typeSpecs };
if (batch) {
importArgs.batch = batch;
}
return baja.registry.importTypes(importArgs)
.then(function (types) {
return Promise.all(types.map(makeWithTypeImplementation));
});
}
/**
* Load the concrete types from the registry, import the types and
* map each one to a new MgrTypeInfo instance.
*
* @inner
* @param baseType
* @param {baja.comm.Batch} batch An optional batch instance, used to combine network calls.
* @returns {Promise}
*/
function getFromBaseType(baseType, batch) {
var getTypeArgs = { type: baseType };
if (batch) { getTypeArgs.batch = batch; }
return baja.registry.getConcreteTypes(getTypeArgs)
.then(function (concreteTypes) {
return baja.registry.importTypes(concreteTypes);
})
.then(function (importedTypes) {
return Promise.all(importedTypes.map(makeWithTypeImplementation));
});
}
return (MgrTypeInfo);
});