define(['mmirf/resources', 'mmirf/logger', 'mmirf/events', 'mmirf/util/loadFile', 'mmirf/util/isArray', 'mmirf/util/deferred', 'module'],
/**
* A class for managing the configuration. <br>
* It's purpose is to load the configuration and settings automatically.
*
* This "class" is structured as a singleton - so that only one instance is in use.<br>
*
* @name ConfigurationManager
* @memberOf mmir
* @static
* @class
* @hideconstructor
*
* @requires mmir.require for getting/setting language code (e.g. see {@link mmir.ConfigurationManager.getLanguage}
*
*/
function (
res, Logger, EventEmitter, loadFile, isArray, deferred, module
){
//the next comment enables JSDoc2 to map all functions etc. to the correct class description
/** @scope ConfigurationManager.prototype */
/**
* Object containing the instance of the class {@link mmir.ConfigurationManager}.
*
* @type Object
* @private
*
* @memberOf mmir.ConfigurationManager#
*/
var instance = null;
/**
* @private
* @type mmir.tools.Logger
* @memberOf mmir.ConfigurationManager#
*/
var logger = Logger.create(module);
/**
* @private
* @type ConfigurationManagerModuleConfig
* @memberOf mmir.ConfigurationManager#
*/
var _conf = module.config(module);
/**
* Constructor-Method of Singleton mmir.ConfigurationManager.
*
* @private
*
* @memberOf mmir.ConfigurationManager#
*/
function constructor(){
/** @scope ConfigurationManager.prototype */
/**
* the configuration data (i.e. properties)
* @type Object
* @private
*
* @memberOf mmir.ConfigurationManager#
*/
var configData = null;
/**
* EventEmitter for change-listeners that will be notified on changes on specific
* configurtion paths (dot-speparated property path)
*
* @private
* @type mmir.tools.EventEmitter
* @memberOf mmir.ConfigurationManager#
*/
var listeners = new EventEmitter(null);
/**
* HELPER for emitting on-change events to listeners
*
* @param {any} newValue the new configuration value
* @param {any} oldValue the old configuration value
* @param {Array<string>} path the configuration path, i.e. list of
* segements of dot-separated path
*
* @private
* @memberOf mmir.ConfigurationManager#
*/
function _emitChange(newValue, oldValue, path){
if(listeners.empty()){
return;///////// EARLY EXIT ////////////
}
var pathStr = isArray(path)? path.join('.') : path;
path = pathStr? _getAsPath(path) : null;
//emit to listeners of "any change" (i.e. empty property-path string):
listeners.emit('', newValue, oldValue, path || []);
if(pathStr){
//emit to listeners of the property-path:
listeners.emit(pathStr, newValue, oldValue, path);
}
}
/**
* Register listener for configuration changes.
*
* @param {String|Array<String>} [propertyName] OPTIONAL
*
* The name of the property, to listen for changes:
* if unspecified, listener will be invoked on all configuration
* changes.
*
* If <code>propertyName</code> is an Array, it
* will be treated as if its entries were path-elements
* analogous to a dot-separated String propertyName.
*
* NOTE: dot-separated names will be resolved into
* object-structures, e.g.
* <code>some.property</code> will be resolved
* so that the <code>value</code> will set to:
* <code>some: {property: <value>}</code>
*
* @param {Function} listener the listener function that will be invoked
* when a configuration value is changed:
* <pre>listener(newValue: any, oldValue: any, propertyName: string[])</pre>
* where <code>propertyName</code> is the list of property-name path components
* (the last component is the property name itself)
* NOTE: if parameter <code>propertyName</code> was not specified, the third argument
* for the listener will be invoked with an empty Array.
*
* @param {Boolean} [emitOnAdding] OPTIONAL if <code>true</code> the listener will be immediately be invoked
* after adding it with the current value
* <pre>listener(newValue: <current value>, oldValue: undefined, propertyName: <propertyName>)</pre>
* NOTE: can only be used when param <code>propertyName</code> is specified.
*
* @private
* @memberOf mmir.ConfigurationManager#
*/
function _onChange(propertyName, listener, emitOnAdding){
if(typeof propertyName === 'function'){
listener = propertyName;
propertyName = void(0);
}
// use empty string as "any change" event type:
var path = propertyName? _getAsPath(propertyName) : null;
var pathStr = propertyName? path.join('.') : '';
listeners.on(pathStr, listener);
if(propertyName && emitOnAdding){
// ASSERT path is
listener(_get(path), void(0), path);
}
}
/**
* Remove listener for configuration changes:
* if listener was registered multiple times, the first one is removed.
*
* @param {String|Array<String>} [propertyName] OPTIONAL
*
* The name of the property, to listen for changes:
* if unspecified, listener will be removed from list of listeners
* for all configuration changes, otherwise it will be removed
* from listeners for the specified property-path.
*
* @param {Function} listener the listener function that will be invoked
* when a configuration value is changed:
* <pre>listener(newValue: any, oldValue: any, propertyName: string)</pre>
*
* @returns {boolean} <code>true</code> if a listener was removed,
* otherwise <code>false</code>.
*
* @private
* @memberOf mmir.ConfigurationManager#
*/
function _offChange(propertyName, listener){
if(typeof propertyName === 'function'){
listener = propertyName;
propertyName = void(0);
}
// use empty string as "any change" event type:
var path = propertyName? _getAsPath(propertyName).join('.') : '';
return listeners.off(path, listener);
}
/**
* Helper that loads configuration file synchronously.
*
* Side-Effect:
* sets {@link #configData} with loaded configuration data (if successful)
*
* @private
* @memberOf mmir.ConfigurationManager#
*
* @param {Function} [callback] callback that is invoke after configuration was loaded
* <pre>callback(configData | null)</pre>
* If loading failed, callback will be invoked with <code>null</null>.
*/
function _loadConfigFile(callback){
if(_conf && _conf.configuration){
logger.verbose("loadConfigFile(): loaded configuration from module.config().configuration");
configData = _conf.configuration;
return;/////////// EARLY EXIT ///////////////
}
loadFile({
async: true,
dataType: "json",
url: res.getConfigurationFileUrl(),
success: function(data){
logger.verbose("loadConfigFile(): loaded configuration from "+res.getConfigurationFileUrl());
if(data){
configData = data;
}
callback && callback(data);
},
error: function(data){
var errStr = "loadConfigFile(): failed to load configuration from '"+res.getConfigurationFileUrl()+"'! ERROR: ";
try{
errStr += JSON.stringify(data);
logger.error(errStr);
}catch(e){
logger.error(errStr, errStr);
}
callback && callback(null);
}
});
}
/**
* "Normalizes" a (dot-separated) string or an array into a path:
* the result is an array of path components (i.e. each path component is a separate entry).
*
* NOTE: if propertyName is an Array, its entries are used as-is, i.e. are
* NOT processed for string-entries that have dot-separating content:
* <pre>
* _getAsPath(['dot', 'separated.list']);//-> returns: ['dot', 'separated.list']
* </pre>
*
* @example
* //result is ['dot', 'separated', 'list']
* _getAsPath('dot.separated.list');
* _getAsPath(['dot', 'separated', 'list']);
*
* @private
* @param {String|Array<String>} propertyName
* resolves a dot-separated property-name into an array.
* If <code>propertyName</code> is an Array, all contained
* String entries will be resolved, if necessary
*
* @returns {Array<String>} list of dot-separated components (without the dots)
*
* @memberOf mmir.ConfigurationManager#
*/
function _getAsPath(propertyName){
return isArray(propertyName)? propertyName : propertyName.split('.');
}
/**
* "Normalizes" an array of Strings by separating
* each String at dots and creating one single (flat) array where
* each path-component is a single entry.
*
* Processes (and flattens) string-entries that have themselves dot-separating
* notation:
* <pre>
* _flattenPath(['dot', 'separated.list']);//-> returns: ['dot', 'separated', 'list']
* </pre>
*
* @private
* @param {String|Array<String>} pathStringOrList
* resolves a dot-separated path or array with paths, i.e. dot-separated property-names
* into a single, flat array where each path component is a separate Strings
*
* @returns {Array<String>} list of dot-separated components (without the dots)
*
* @memberOf mmir.ConfigurationManager#
*
* //result is ['dot', 'separated', 'list']
* _toPath(['dot', 'separated.list']);
* _toPath('dot.separated.list');
* _toPath(['dot', 'separated', 'list']);
*/
function _toPath(pathStringOrList){
var pathList = isArray(pathStringOrList)? pathStringOrList : pathStringOrList.split('.');
var entry;
var increase = 0;
var size = pathList.length;
var tempPath;
for(var i=0; i < size; ++i){
entry = pathList[i];
tempPath = entry.split('.');
//if entry contained dot-separated path:
// replace original entry with the new sub-path
if(tempPath.length > 1){
pathList[i] = tempPath;
increase += (tempPath.length - 1);
}
}
//if sup-paths were inserted: flatten the array
if(increase > 0){
//create new array that can hold all entries
var newPath = new Array(size + increase);
var index = 0;
for(var i=0; i < size; ++i){
entry = pathList[i];
//flatten sub-paths into the new array:
if( isArray(entry) ){
for(var j=0, len=entry.length; j < len; ++j){
newPath[index++] = entry[j];
}
}
else {
//for normal entries: just insert
newPath[index++] = entry;
}
}
pathList = newPath;
}
return pathList;
}
/**
* Returns the value of a property.
*
* @function
* @param {String|Array<String>} propertyName
* if String: The name of the property.
* NOTE: If the property does not exists at the root-level,
* dot-separated names will be resolved into
* object-structures, e.g.
* <code>some.property</code> will be resolved
* so that the <code>value</code> at:
* <code>some: {property: <value>}</code>
* will be returned
* if String array: each entry corresponds to component in a
* dot-separated path (see above)
* @param {any} [defaultValue] OPTIONAL
* a default value that will be returned, in case there is no property
* <code>propertyName</code>.
* @param {Boolean} [setAsDefaultIfUnset] OPTIONAL
* if <code>true</code>, and there is no value set yet for <code>propertyName</code>,
* then the specified <code>defaultValue</code> will be set for <code>propertyName</code>.
*
* <br>DEFAULT: <code>false</code>
* <br>NOTE: if this argument is used, param <code>defaultValue</code> must also be given!
*
* @returns {any}
* The value of the property
* @private
* @memberOf mmir.ConfigurationManager#
*/
function _get(propertyName, defaultValue, setAsDefaultIfUnset){
if(configData){
if(typeof configData[propertyName] !== 'undefined'){
return configData[propertyName];///////////// EARLY EXIT /////////////////////
}
var path = _getAsPath(propertyName);
if(typeof configData[ path[0] ] === 'undefined'){
return !setAsDefaultIfUnset? defaultValue : _set(path, defaultValue);///////////// EARLY EXIT /////////////////////
}
var obj = configData, prop;
for(var i = 0, size = path.length, len = size - 1; i < size; ++i){
prop = path[i];
obj = obj[prop];
if(typeof obj === 'undefined'){
return !setAsDefaultIfUnset? defaultValue : _set(path, defaultValue);///////////// EARLY EXIT /////////////////////
} else if(i === len){
return obj;///////////// EARLY EXIT /////////////////////
}
}
}
return defaultValue;
}
/**
* Sets a property to a given value.
*
* @function
* @param {String|Array<String>} propertyName
*
* The name of the property.
*
* If <code>propertyName</code> is an Array, it
* will be treated as if its entries were path-elements
* analogous to a dot-separated String propertyName.
*
* NOTE: dot-separated names will be resolved into
* object-structures, e.g.
* <code>some.property</code> will be resolved
* so that the <code>value</code> will set to:
* <code>some: {property: <value>}</code>
*
* @param {any} value
* The value of the property
*
* @returns {any}
* The newly set value for the property
*
* @throws {Error} if the propertyName is dot-separated AND
* one of its path-elements (except for the last)
* already exists AND its type is not 'object'
*
* @private
* @memberOf mmir.ConfigurationManager#
*/
function _set(propertyName, value){
if(!configData){
configData = {};
}
var path = _getAsPath(propertyName);
var oldVal;
if(path.length > 1){
var obj = configData, prop;
for(var i = 0, size = path.length, len = size - 1; i < size; ++i){
prop = path[i];
if(i === len){
oldVal = obj[prop];
obj[prop] = value;
_emitChange(value, oldVal, path);
}
else if(typeof obj[prop] === 'undefined' || obj[prop] === null){
obj[prop] = {};
}
else if(typeof obj[prop] !== 'object' && i < size - 1){
throw new Error('Cannot expand path "'+propertyName+'": path-element "'+prop+'" already exist and has type "'+(typeof obj[prop])+'"');
}
obj = obj[prop];
}
} else {
oldVal = configData[propertyName];
configData[propertyName] = value;
_emitChange(value, oldVal, path);
}
return value;
}
/** @lends mmir.ConfigurationManager.prototype */
return {
/**
* Initialize promise that will be resolve after initialization is completed
* (set in {@link #init}).
*
* @private
* @memberOf mmir.ConfigurationManager.prototype
* @type {Promise}
*/
_initialized: null,
// public members
/**
* Initialize the configuration manager (i.e. loading configuration data etc.)
*
* @public
* @function
* @async
* @memberOf mmir.ConfigurationManager.prototype
*
* @param {Boolean} [forceReloadConfig] OPTIONAL
* if FALSY, configuration data will only be loaded, if it
* has not been loaded yet.
* If TRUTHY, configuration data will be reloaded regardless,
* wether it is already available or not.
*
* @returns {Promise}
* a deferred promise that gets fulfilled when initialization is completed.
*/
init: function(forceReloadConfig){
if(!this._initialized || forceReloadConfig){
this._initialized = deferred();
var self = this;
_loadConfigFile(function(){
self._initialized.resolve(self);
});
}
return this._initialized;
},
/**
* @copydoc mmir.ConfigurationManager#_get
* @function
* @public
* @memberOf mmir.ConfigurationManager.prototype
* @see #set
*/
get: _get,
/**
* @copydoc mmir.ConfigurationManager#_set
* @function
* @public
* @memberOf mmir.ConfigurationManager.prototype
* @see #get
*/
set: _set,
/**
* Uses {@link #get}.
*
* If the propertyName does not exists, returns <code>undefined</code>,
* otherwise values will be converted into Boolean values.
*
* Special case for Strings:
* the String <code>"false"</code> will be converted to
* Boolean value <code>false</code>.
*
* @public
* @param {any} [defaultValue] OPTIONAL
*
* if a default value is specified and there exists
* no property <code>propertyName</code>, the
* specified default value will be returned.
*
* NOTE: the default value will also be converted
* to a Boolean value, if necessary.
*
* @see #get
* @memberOf mmir.ConfigurationManager.prototype
*/
getBoolean: function(propertyName, defaultValue, setAsDefaultIfUnset){
var val = this.get(propertyName, defaultValue, setAsDefaultIfUnset);
if(typeof val !== 'undefined'){
if( val === 'false'){
return false;
}
else {
return val? true : false;
}
}
},
/**
* Uses {@link #get}.
*
* If the property does not exists, returns <code>undefined</code>,
* otherwise values will be converted into String values.
*
* If the value has not the type <code>"string"</code>, it will
* be converted by <code>JSON.stringify</code>.
*
* @public
* @param {any} [defaultValue] OPTIONAL
* if a default value is specified and there exists
* no property <code>propertyName</code>, the
* specified default value will be returned.
*
* NOTE: the default value will also be converted
* to a String value, if necessary.
*
* @see #get
* @memberOf mmir.ConfigurationManager.prototype
*/
getString: function(propertyName, defaultValue, setAsDefaultIfUnset){
var val = this.get(propertyName, defaultValue, setAsDefaultIfUnset);
if(typeof val !== 'undefined'){
if(typeof val === 'string'){
return val;
}
else {
return JSON.stringify(val);
}
}
},
/**
* Uses {@link #get}.
*
* If the property does not exists, returns <code>undefined</code>,
* otherwise values will be converted into Number values.
*
* If the value has not the type <code>"string"</code>, it will
* be converted by <code>JSON.stringify</code>.
*
* @public
* @param {any} [defaultValue] OPTIONAL
* if a default value is specified and there exists
* no property <code>propertyName</code>, the
* specified default value will be returned.
*
* NOTE: the default value will also be converted
* to a Number value, if necessary.
*
* @see #get
* @memberOf mmir.ConfigurationManager.prototype
*/
getNumber: function(propertyName, defaultValue, setAsDefaultIfUnset){
var val = this.get(propertyName, defaultValue, setAsDefaultIfUnset);
if(typeof val !== 'undefined'){
if(typeof val === 'number'){
return val;
}
else {
return parseFloat(val);
}
}
},
/**
* @copydoc mmir.ConfigurationManager#_onChange
* @public
* @function
* @memberOf mmir.ConfigurationManager.prototype
* @see #on
*/
addListener: _onChange,
/**
* @copydoc mmir.ConfigurationManager#_offChange
* @public
* @function
* @memberOf mmir.ConfigurationManager.prototype
* @see #off
*/
removeListener: _offChange,
/**
* @copydoc mmir.ConfigurationManager#_onChange
* @public
* @function
* @memberOf mmir.ConfigurationManager.prototype
* @see #addListener
*/
on: _onChange,
/**
* @copydoc mmir.ConfigurationManager#_offChange
* @public
* @function
* @memberOf mmir.ConfigurationManager.prototype
* @see #removeListener
*/
off: _offChange,
/**
* @copydoc mmir.ConfigurationManager#_toPath
* @function
* @public
* @memberOf mmir.ConfigurationManager.prototype
* @see #get
*/
toPath: _toPath,
};//END: return {...
}//END: constructor = function(){...
instance = new constructor();
//immediately start initalization (i.e. loading of configuration data etc.):
instance.init();
return instance;
});