Source: manager/settings/configurationManager.js

  1. define(['mmirf/resources', 'mmirf/logger', 'mmirf/events', 'mmirf/util/loadFile', 'mmirf/util/isArray', 'module'],
  2. /**
  3. * A class for managing the configuration. <br>
  4. * It's purpose is to load the configuration and settings automatically.
  5. *
  6. * This "class" is structured as a singleton - so that only one instance is in use.<br>
  7. *
  8. * @name ConfigurationManager
  9. * @memberOf mmir
  10. * @static
  11. * @class
  12. * @hideconstructor
  13. *
  14. * @requires mmir.require for getting/setting language code (e.g. see {@link mmir.ConfigurationManager.getLanguage}
  15. *
  16. */
  17. function (
  18. res, Logger, EventEmitter, loadFile, isArray, module
  19. ){
  20. //the next comment enables JSDoc2 to map all functions etc. to the correct class description
  21. /** @scope ConfigurationManager.prototype */
  22. /**
  23. * Object containing the instance of the class {@link mmir.ConfigurationManager}.
  24. *
  25. * @type Object
  26. * @private
  27. *
  28. * @memberOf mmir.ConfigurationManager#
  29. */
  30. var instance = null;
  31. /**
  32. * @private
  33. * @type mmir.tools.Logger
  34. * @memberOf mmir.ConfigurationManager#
  35. */
  36. var logger = Logger.create(module);
  37. /**
  38. * @private
  39. * @type ConfigurationManagerModuleConfig
  40. * @memberOf mmir.ConfigurationManager#
  41. */
  42. var _conf = module.config(module);
  43. /**
  44. * Constructor-Method of Singleton mmir.ConfigurationManager.
  45. *
  46. * @private
  47. *
  48. * @memberOf mmir.ConfigurationManager#
  49. */
  50. function constructor(){
  51. /** @scope ConfigurationManager.prototype */
  52. /**
  53. * the configuration data (i.e. properties)
  54. * @type Object
  55. * @private
  56. *
  57. * @memberOf mmir.ConfigurationManager#
  58. */
  59. var configData = null;
  60. /**
  61. * EventEmitter for change-listeners that will be notified on changes on specific
  62. * configurtion paths (dot-speparated property path)
  63. *
  64. * @private
  65. * @type mmir.tools.EventEmitter
  66. * @memberOf mmir.ConfigurationManager#
  67. */
  68. var listeners = new EventEmitter(null);
  69. /**
  70. * HELPER for emitting on-change events to listeners
  71. *
  72. * @param {any} newValue the new configuration value
  73. * @param {any} oldValue the old configuration value
  74. * @param {Array<string>} path the configuration path, i.e. list of
  75. * segements of dot-separated path
  76. *
  77. * @private
  78. * @memberOf mmir.ConfigurationManager#
  79. */
  80. function _emitChange(newValue, oldValue, path){
  81. if(listeners.empty()){
  82. return;///////// EARLY EXIT ////////////
  83. }
  84. var pathStr = isArray(path)? path.join('.') : path;
  85. //emit to listeners of "any change" (i.e. empty property-path string):
  86. listeners.emit('', newValue, oldValue, pathStr);
  87. if(pathStr){
  88. //emit to listeners of the property-path:
  89. listeners.emit(pathStr, newValue, oldValue, pathStr);
  90. }
  91. }
  92. /**
  93. * Register listener for configuration changes.
  94. *
  95. * @param {String|Array<String>} [propertyName] OPTIONAL
  96. *
  97. * The name of the property, to listen for changes:
  98. * if unspecified, listener will be invoked on all configuration
  99. * changes.
  100. *
  101. * If <code>propertyName</code> is an Array, it
  102. * will be treated as if its entries were path-elements
  103. * analogous to a dot-separated String propertyName.
  104. *
  105. * NOTE: dot-separated names will be resolved into
  106. * object-structures, e.g.
  107. * <code>some.property</code> will be resolved
  108. * so that the <code>value</code> will set to:
  109. * <code>some: {property: &lt;value&gt;}</code>
  110. *
  111. * @param {Function} listener the listener function that will be invoked
  112. * when a configuration value is changed:
  113. * <pre>listener(newValue: any, oldValue: any, propertyName: string)</pre>
  114. *
  115. * @private
  116. * @memberOf mmir.ConfigurationManager#
  117. */
  118. function _onChange(propertyName, listener){
  119. if(typeof propertyName === 'function'){
  120. listener = propertyName;
  121. propertyName = void(0);
  122. }
  123. // use empty string as "any change" event type:
  124. var path = propertyName? _getAsPath(propertyName).join('.') : '';
  125. listeners.on(path, listener);
  126. }
  127. /**
  128. * Remove listener for configuration changes:
  129. * if listener was registered multiple times, the first one is removed.
  130. *
  131. * @param {String|Array<String>} [propertyName] OPTIONAL
  132. *
  133. * The name of the property, to listen for changes:
  134. * if unspecified, listener will be removed from list of listeners
  135. * for all configuration changes, otherwise it will be removed
  136. * from listeners for the specified property-path.
  137. *
  138. * @param {Function} listener the listener function that will be invoked
  139. * when a configuration value is changed:
  140. * <pre>listener(newValue: any, oldValue: any, propertyName: string)</pre>
  141. *
  142. * @returns {boolean} <code>true</code> if a listener was removed,
  143. * otherwise <code>false</code>.
  144. *
  145. * @private
  146. * @memberOf mmir.ConfigurationManager#
  147. */
  148. function _offChange(propertyName, listener){
  149. if(typeof propertyName === 'function'){
  150. listener = propertyName;
  151. propertyName = void(0);
  152. }
  153. // use empty string as "any change" event type:
  154. var path = propertyName? _getAsPath(propertyName).join('.') : '';
  155. return listeners.off(path, listener);
  156. }
  157. /**
  158. * Helper that loads configuration file synchronously.
  159. *
  160. * @private
  161. * @memberOf mmir.ConfigurationManager#
  162. */
  163. //FIXME change implementation to async-loading?
  164. // -> would need to add init()-function, with callback and/or return Deferred
  165. function _loadConfigFile(){
  166. if(_conf && _conf.configuration){
  167. logger.verbose("loadConfigFile(): loaded configuration from module.config().configuration");
  168. configData = _conf.configuration;
  169. return;/////////// EARLY EXIT ///////////////
  170. }
  171. loadFile({
  172. async: false,
  173. dataType: "json",
  174. url: res.getConfigurationFileUrl(),
  175. success: function(data){
  176. logger.verbose("loadConfigFile(): loaded configuration from "+res.getConfigurationFileUrl());
  177. if(data){
  178. configData = data;
  179. }
  180. },
  181. error: function(data){
  182. var errStr = "loadConfigFile(): failed to load configuration from '"+res.getConfigurationFileUrl()+"'! ERROR: ";
  183. try{
  184. errStr += JSON.stringify(data);
  185. logger.error(errStr);
  186. }catch(e){
  187. logger.error(errStr, errStr);
  188. }
  189. }
  190. });
  191. }
  192. //immediately load the configuration:
  193. _loadConfigFile();
  194. /**
  195. * "Normalizes" a string or an array into a path:
  196. * the result is a single, flat array where each string has
  197. * been separated at dots (i.e. each path component is a separate entry).
  198. *
  199. * @example
  200. * //result is ['dot', 'separated', 'list']
  201. * _getAsPath('dot.separated.list');
  202. * _getAsPath(['dot', 'separated.list']);
  203. * _getAsPath(['dot', 'separated', 'list']);
  204. *
  205. * @private
  206. * @param {String|Array<String>} propertyName
  207. * resolves a dot-separated property-name into an array.
  208. * If <code>propertyName</code> is an Array, all contained
  209. * String entries will be resolved, if necessary
  210. *
  211. * @memberOf mmir.ConfigurationManager#
  212. */
  213. function _getAsPath(propertyName){
  214. var path = propertyName;
  215. if( ! isArray(path)){
  216. path = propertyName.split('.');
  217. }
  218. else {
  219. path = _toPathArray(propertyName);
  220. }
  221. return path;
  222. }
  223. /**
  224. * "Normalizes" an array of Strings by separating
  225. * each String at dots and creating one single (flat) array where
  226. * each path-component is a single entry.
  227. *
  228. * @private
  229. * @param {Array<String>} pathList
  230. * resolves an array with paths, i.e. dot-separated property-names
  231. * into a single, flat array where each path component is a separate Strings
  232. *
  233. * @memberOf mmir.ConfigurationManager#
  234. */
  235. function _toPathArray(pathList){
  236. var entry;
  237. var increase = 0;
  238. var size = pathList.length;
  239. var tempPath;
  240. for(var i=0; i < size; ++i){
  241. entry = pathList[i];
  242. tempPath = entry.split('.');
  243. //if entry contained dot-separated path:
  244. // replace original entry with the new sub-path
  245. if(tempPath.length > 1){
  246. pathList[i] = tempPath;
  247. increase += (tempPath.length - 1);
  248. }
  249. }
  250. //if sup-paths were inserted: flatten the array
  251. if(increase > 0){
  252. //create new array that can hold all entries
  253. var newPath = new Array(size + increase);
  254. var index = 0;
  255. for(var i=0; i < size; ++i){
  256. entry = pathList[i];
  257. //flatten sub-paths into the new array:
  258. if( isArray(entry) ){
  259. for(var j=0, len=entry.length; j < len; ++j){
  260. newPath[index++] = entry[j];
  261. }
  262. }
  263. else {
  264. //for normal entries: just insert
  265. newPath[index++] = entry;
  266. }
  267. }
  268. pathList = newPath;
  269. }
  270. return pathList;
  271. }
  272. /** @lends mmir.ConfigurationManager.prototype */
  273. return {
  274. // public members
  275. /**
  276. * Returns the value of a property.
  277. *
  278. * @function
  279. * @param {String|Array<String>} propertyName
  280. * if String: The name of the property.
  281. * NOTE: If the property does not exists at the root-level,
  282. * dot-separated names will be resolved into
  283. * object-structures, e.g.
  284. * <code>some.property</code> will be resolved
  285. * so that the <code>value</code> at:
  286. * <code>some: {property: &lt;value&gt;}</code>
  287. * will be returned
  288. * if String array: each entry corresponds to component in a
  289. * dot-separated path (see above)
  290. * @param {any} [defaultValue] OPTIONAL
  291. * a default value that will be returned, in case there is no property
  292. * <code>propertyName</code>.
  293. * @param {Boolean} [useSafeAccess] OPTIONAL
  294. * if <code>true</code>, resolution of dot-separated paths
  295. * will be done "safely", i.e. if a path-element does not
  296. * exists, no <code>error</code> will be thrown, but instead
  297. * the function will return the <code>defaultValue</code>
  298. * (which will be <code>undefined</code> if the argument is not given).
  299. *
  300. * <br>DEFAULT: <code>true</code>
  301. * <br>NOTE: if this argument is used, param <code>defaultValue</code> must also be given!
  302. *
  303. * @returns {any}
  304. * The value of the property
  305. * @public
  306. * @memberOf mmir.ConfigurationManager.prototype
  307. */
  308. get: function(propertyName, defaultValue, useSafeAccess){
  309. if(configData){
  310. if(typeof configData[propertyName] !== 'undefined'){
  311. return configData[propertyName];//////////// EARLY EXIT ///////////////////
  312. }
  313. var path = _getAsPath(propertyName);
  314. if(typeof useSafeAccess === 'undefined'){
  315. useSafeAccess = true;
  316. }
  317. if(useSafeAccess && typeof configData[ path[0] ] === 'undefined'){
  318. return defaultValue;///////////// EARLY EXIT /////////////////////////
  319. }
  320. var obj = configData, prop;
  321. for(var i = 0, size = path.length, len = size - 1; i < size; ++i){
  322. // while(path.length > 1){
  323. prop = path[i];
  324. obj = obj[prop];
  325. if(useSafeAccess && typeof obj === 'undefined'){
  326. return defaultValue;///////////// EARLY EXIT /////////////////////
  327. } else if(i === len){
  328. return obj;///////////// EARLY EXIT /////////////////////
  329. }
  330. }
  331. }
  332. return defaultValue;
  333. },
  334. /**
  335. * Sets a property to a given value.
  336. *
  337. * @function
  338. * @param {String|Array<String>} propertyName
  339. *
  340. * The name of the property.
  341. *
  342. * If <code>propertyName</code> is an Array, it
  343. * will be treated as if its entries were path-elements
  344. * analogous to a dot-separated String propertyName.
  345. *
  346. * NOTE: dot-separated names will be resolved into
  347. * object-structures, e.g.
  348. * <code>some.property</code> will be resolved
  349. * so that the <code>value</code> will set to:
  350. * <code>some: {property: &lt;value&gt;}</code>
  351. *
  352. * @param {any} value
  353. * The value of the property
  354. *
  355. * @throws {Error} if the propertyName is dot-separated AND
  356. * one of its path-elements (except for the last)
  357. * already exists AND its type is not 'object'
  358. *
  359. * @public
  360. * @memberOf mmir.ConfigurationManager.prototype
  361. */
  362. set: function(propertyName, value){
  363. if(!configData){
  364. configData = {};
  365. }
  366. var path = _getAsPath(propertyName);
  367. var oldVal;
  368. if(path.length > 1){
  369. var obj = configData, prop;
  370. for(var i = 0, size = path.length, len = size - 1; i < size; ++i){
  371. prop = path[i];
  372. if(i === len){
  373. oldVal = obj[prop];
  374. obj[prop] = value;
  375. _emitChange(value, oldVal, path);
  376. }
  377. else if(typeof obj[prop] === 'undefined' || obj[prop] === null){
  378. obj[prop] = {};
  379. }
  380. else if(typeof obj[prop] !== 'object' && i < size - 1){
  381. throw new Error('Cannot expand path "'+propertyName+'": path-element "'+prop+'" already exist and has type "'+(typeof obj[prop])+'"');
  382. }
  383. obj = obj[prop];
  384. }
  385. }
  386. else {
  387. oldVal = configData[propertyName];
  388. configData[propertyName] = value;
  389. _emitChange(value, oldVal, path);
  390. }
  391. },
  392. /**
  393. * Uses {@link #get}.
  394. *
  395. * If the propertyName does not exists, returns <code>undefined</code>,
  396. * otherwise values will be converted into Boolean values.
  397. *
  398. * Special case for Strings:
  399. * the String <code>"false"</code> will be converted to
  400. * Boolean value <code>false</code>.
  401. *
  402. * @public
  403. * @param {any} [defaultValue] OPTIONAL
  404. *
  405. * if a default value is specified and there exists
  406. * no property <code>propertyName</code>, the
  407. * specified default value will be returned.
  408. *
  409. * NOTE: if this argument is used, <code>useSafeAccess</code> must also be given!
  410. *
  411. * NOTE: the default value will also be converted
  412. * to a Boolean value, if necessary.
  413. *
  414. * @see #get
  415. * @memberOf mmir.ConfigurationManager.prototype
  416. */
  417. getBoolean: function(propertyName, defaultValue, useSafeAccess){
  418. var val = this.get(propertyName, defaultValue, useSafeAccess);
  419. if(typeof val !== 'undefined'){
  420. if( val === 'false'){
  421. return false;
  422. }
  423. else {
  424. return val? true : false;
  425. }
  426. }
  427. },
  428. /**
  429. * Uses {@link #get}.
  430. *
  431. * If the property does not exists, returns <code>undefined</code>,
  432. * otherwise values will be converted into String values.
  433. *
  434. * If the value has not the type <code>"string"</code>, it will
  435. * be converted by <code>JSON.stringify</code>.
  436. *
  437. * @public
  438. * @param {any} [defaultValue] OPTIONAL
  439. * if a default value is specified and there exists
  440. * no property <code>propertyName</code>, the
  441. * specified default value will be returned.
  442. *
  443. * NOTE: if this argument is used, <code>useSafeAccess</code> must also be given!
  444. *
  445. * NOTE: the default value will also be converted
  446. * to a String value, if necessary.
  447. *
  448. * @see #get
  449. * @memberOf mmir.ConfigurationManager.prototype
  450. */
  451. getString: function(propertyName, defaultValue, useSafeAccess){
  452. var val = this.get(propertyName, defaultValue, useSafeAccess);
  453. if(typeof val !== 'undefined'){
  454. if(typeof val === 'string'){
  455. return val;
  456. }
  457. else {
  458. return JSON.stringify(val);
  459. }
  460. }
  461. },
  462. /**
  463. * @copydoc mmir.ConfigurationManager#_onChange
  464. * @public
  465. * @function
  466. * @memberOf mmir.ConfigurationManager.prototype
  467. * @see #on
  468. */
  469. addListener: _onChange,
  470. /**
  471. * @copydoc mmir.ConfigurationManager#_offChange
  472. * @public
  473. * @function
  474. * @memberOf mmir.ConfigurationManager.prototype
  475. * @see #off
  476. */
  477. removeListener: _offChange,
  478. /**
  479. * @copydoc mmir.ConfigurationManager#_onChange
  480. * @public
  481. * @function
  482. * @memberOf mmir.ConfigurationManager.prototype
  483. * @see #addListener
  484. */
  485. on: _onChange,
  486. /**
  487. * @copydoc mmir.ConfigurationManager#_offChange
  488. * @public
  489. * @function
  490. * @memberOf mmir.ConfigurationManager.prototype
  491. * @see #removeListener
  492. */
  493. off: _offChange
  494. };//END: return {...
  495. }//END: constructor = function(){...
  496. instance = new constructor();
  497. return instance;
  498. });