const ModuleAnalyzer = require('./ModuleAnalyzer');
const ConfigResolver = require('./ConfigResolver');
const Singleton = require('./instantiators/Singleton');
const Prototype = require('./instantiators/Prototype');
const DependencyTree = require('./DependencyTree');
const ModuleResolver = require('./ModuleResolver');
const PrototypeWrapper = require('./instantiators/PrototypeWrapper');
/**
* The ObjectManager is the central component which instantiates modules,
* which may be requested by other objects.
* The ObjectManager works with dependency trees. Each dependency tree
* has an identifier. The default tree has the identifier "root". If you
* need a different tree or want to branch the tree so the depending
* instances have a different set of dependencies, you need either need
* to call the requestInstance method with a different treeIdentifier
* or you define the treeIdentifier in the dependency definition of
* your module.
*
* To request an instance, just call getInstance(moduleName)
*
* @author Wolfgang Felbermeier (@f3lang)
*/
class ObjectManager {
/**
* @param {Object} options The configuration options
* @param {Array} options.moduleSrc An array with paths to the module source files to autowire
* @param {String=} options.cacheFile A file to use as a cache for the module meta data
* @param {Object=} options.configurations A set of configurations to use
* @param {boolean=} [options.globalScope=false] When set to true, will register the current instance
* in the global scope and return it, when requested again. Use this only when necessary and you need to
* use the object manager in different locations that cannot be resolved in one scope by the ObjectManager
*/
constructor(options) {
let defaultOptions = {
moduleSrc: [],
cacheFile: null,
configurations: {},
globalScope: false
};
this.options = Object.assign(defaultOptions, options || {});
if (this.options.globalScope) {
if (global._cid) {
return global._cid;
} else {
global._cid = this;
}
}
this.trees = {};
this.configurations = {};
this.instantiators = {};
this.moduleAnalyzer = new ModuleAnalyzer(this);
this.configResolver = new ConfigResolver();
this.addConfiguration({moduleSrc: this.options.moduleSrc, cacheFile: this.options.cacheFile}, 'cdi');
Object.keys(this.options.configurations).forEach(key => {
this.addConfiguration(options.configurations[key], key);
});
this.moduleResolver = new ModuleResolver(this.options.moduleSrc, this.options.cacheFile, this.moduleAnalyzer);
this.prototypeWrapper = new PrototypeWrapper(this);
this.registerInstantiator('singleton', new Singleton(this, this.prototypeWrapper));
this.registerInstantiator('prototype', new Prototype(this, this.prototypeWrapper));
this.addDependencyTree('root');
this.nextRequestID = 0;
this.connectedObjectManagers = [];
}
/**
* Adds a configuration to the Object Manager
* @param {object} config The configuration object
* @param {String=} root The configuration root, that can be used to inject settings.
*/
addConfiguration(config, root = "") {
if (this.configurations[root]) {
throw new Error('[cdi] [ObjectManager] Identifier "' + root
+ '" has already been used by another configuration');
}
this.configurations[root] = config;
this.configResolver.addConfiguration(config, root);
}
/**
* Returns an instance of a module by name. All dependencies will be injected.
* @param {string} moduleName The name of the module. Must match the Module name or one of its alias
* @param {string=} tree The tree to use. Defaults to "root"
* @return {*} The instance of the module
*/
getInstance(moduleName, tree) {
return this.requestInstanceOfModule(moduleName, tree);
}
/**
* Requests an instance of a module. All dependencies will be injected also into submodules.
* @param {string} moduleName The name of the module. Must match the Module name or one of its alias
* @param {string=} treeIdentifier The tree to use. Default is "root"
* @param {int=} _requestId Internal parameter to detect circular dependencies
* @return {*}
*/
requestInstanceOfModule(moduleName, treeIdentifier = 'root', _requestId = this.nextRequestID++) {
switch (moduleName) {
case 'ObjectManager':
return this;
case 'DependencyTree':
if (!this.trees[treeIdentifier]) {
this.addDependencyTree(treeIdentifier);
}
return this.trees[treeIdentifier];
case 'ConfigResolver':
return this.configResolver;
case 'ModuleAnalyzer':
return this.moduleAnalyzer;
case 'ModuleResolver':
return this.moduleResolver;
default:
if (!this.moduleResolver.getResolvedModules().moduleMap[moduleName]) {
let targetObjectManager = this.getContainingObjectManager(moduleName);
if (targetObjectManager) {
return targetObjectManager.requestInstanceOfModule(moduleName, treeIdentifier, _requestId);
}
}
if (!this.trees[treeIdentifier]) {
this.addDependencyTree(treeIdentifier);
}
return this.trees[treeIdentifier].getInstance(moduleName, _requestId);
}
}
/**
* Generates an injector definition for a configuration setting.
* @param {string} configurationIdentifier The configuration identifier of the module
* @return {{type: string, path: *|string, root: *|string, identifier: *}}
*/
getConfigInjector(configurationIdentifier) {
let configPath = configurationIdentifier.split(':');
return {
type: 'config',
path: configPath[2],
root: configPath[1],
identifier: configurationIdentifier
}
}
/**
* Generates an injector definition for the injection of a module
* @param {string} moduleIdentifier The module identifier
* @return {{type: string, tree: string, module: string, moduleIdentifier: string}}
*/
getModuleInjector(moduleIdentifier) {
let tree = '';
if (moduleIdentifier.indexOf(':') > 0) {
tree = moduleIdentifier.substring(moduleIdentifier.indexOf(':') + 1);
}
let moduleName = moduleIdentifier.indexOf(':') > 0 ? moduleIdentifier.substring(0, moduleIdentifier.indexOf(':')) : moduleIdentifier;
return {
type: 'module',
tree,
module: moduleName,
moduleIdentifier: moduleName + ":" + tree
};
}
/**
* Will resolve all instances and configurations needed to instantiate a module.
* @param {Array} injectors The injector definitions to resolve
* @param {string} root The tree to request the injectable parameters from
* @param {string} requestId The unique requestId to discover circular dependencies
* @return {*|{}|Uint8Array|any[]|Int32Array|Uint16Array}
*/
getModuleParams(injectors, root, requestId) {
return injectors.map(injector => {
switch (injector.type) {
case 'module':
return this.requestInstanceOfModule(injector.module, injector.tree || root, requestId);
case 'config':
return this.configResolver.getConfig(injector.root, injector.path);
}
});
}
/**
* Registers a new Instantiator
* @param {string} name The id which this instantiator is used with.
* @param {AbstractInstantiator} instantiator An instance of the instantiator
*/
registerInstantiator(name, instantiator) {
this.instantiators[name] = instantiator;
}
/**
* @param {String} name The Name of the instantiator
* @return {AbstractInstantiator}
*/
getInstantiator(name) {
if (!this.instantiators[name]) {
throw new Error('No instantiator with the identifier "' + name + '" found in ' + JSON.stringify(Object.keys(this.instantiators)));
}
return this.instantiators[name];
}
/**
* Adds a new dependency tree to the stack
* @param {string} root The name of the tree
*/
addDependencyTree(root) {
if (this.trees[root]) {
throw new Error('Tree with root "' + root + '" already exists');
}
this.trees[root] = new DependencyTree(this, this.moduleResolver, root);
}
/**
* Returns the injection configuration of a module
* @param {String} moduleName The name of the module
* @return {InjectionConfiguration}
*/
getInjectionConfiguration(moduleName) {
return this.moduleResolver.getResolvedModules().moduleMap[moduleName];
}
/**
* Connect another object manager to this instance. Local
* resolvable modules will have precedence before modules
* of additional object managers.
* @param {ObjectManager} objectManager The object manager to connect
*/
connectObjectManager(objectManager) {
if (this.connectedObjectManagers.indexOf(objectManager) < 0) {
this.connectedObjectManagers.push(objectManager);
}
}
/**
* Get the object manager which can create an instance of the module.
* If no available object manager including this instance can resolve the
* module, nothing is returned.
* @param {string} moduleName
* @return {ObjectManager}
*/
getContainingObjectManager(moduleName) {
if (this.moduleResolver.getResolvedModules().moduleMap[moduleName]) {
return this;
}
for (let i = 0; i < this.connectedObjectManagers.length; i++) {
if (this.connectedObjectManagers[i].getContainingObjectManager(moduleName)) {
return this.connectedObjectManagers[i];
}
}
}
}
module.exports = ObjectManager;