1 /*
  2  * 	Copyright (C) 2012-2013 DFKI GmbH
  3  * 	Deutsches Forschungszentrum fuer Kuenstliche Intelligenz
  4  * 	German Research Center for Artificial Intelligence
  5  * 	http://www.dfki.de
  6  * 
  7  * 	Permission is hereby granted, free of charge, to any person obtaining a 
  8  * 	copy of this software and associated documentation files (the 
  9  * 	"Software"), to deal in the Software without restriction, including 
 10  * 	without limitation the rights to use, copy, modify, merge, publish, 
 11  * 	distribute, sublicense, and/or sell copies of the Software, and to 
 12  * 	permit persons to whom the Software is furnished to do so, subject to 
 13  * 	the following conditions:
 14  * 
 15  * 	The above copyright notice and this permission notice shall be included 
 16  * 	in all copies or substantial portions of the Software.
 17  * 
 18  * 	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 19  * 	OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 20  * 	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 21  * 	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 22  * 	CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 23  * 	TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 24  * 	SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25  */
 26 	
 27 
 28 //TODO additional dependency on LanguageManager for 
 29 //		* getLanguage() -> languageManager.getLanguage()
 30 //		* setLanguage(lang) -> languageManager.setLanguage(lang)
 31 //
 32 // should the dependency on LanguageManager be made OPTIONAL?
 33 //
 34 define(['constants', 'jquery' ],
 35 	/**
 36 	 * A class for managing the configuration. <br>
 37 	 * It's purpose is to load the configuration and settings automatically.
 38 	 * 
 39 	 * This "class" is structured as a singleton - so that only one instance is in use.<br>
 40 	 * 
 41 	 * @name ConfigurationManager
 42 	 * @memberOf mmir 
 43 	 * @static
 44 	 * @class
 45 	 * 
 46 	 * @requires jQuery.ajax
 47 	 * @requires jQuery.isArray
 48 	 * 
 49 	 */
 50 	function (
 51 		constants, $
 52 ){
 53 	
 54 	//the next comment enables JSDoc2 to map all functions etc. to the correct class description
 55 	/** @scope ConfigurationManager.prototype */
 56 
 57     /**
 58      * Object containing the instance of the class {@link mmir.ConfigurationManager}.
 59      * 
 60      * @type Object
 61      * @private
 62      * 
 63 	 * @memberOf ConfigurationManager#
 64      */
 65     var instance = null;
 66     
 67 	/**
 68 	 * Constructor-Method of Singleton mmir.ConfigurationManager.
 69 	 * 
 70 	 * @private
 71 	 * 
 72 	 * @memberOf ConfigurationManager#
 73 	 */
 74     function constructor(){
 75 
 76     	/** @scope ConfigurationManager.prototype */
 77     	
 78     	/**
 79     	 * the configuration data (i.e. properties)
 80 		 * @type Object
 81 		 * @private
 82 		 * 
 83 		 * @memberOf ConfigurationManager#
 84     	 */
 85     	var configData = null;
 86     	
 87     	
 88     	/**
 89     	 * Helper that loads configuration file synchronously.
 90     	 * 
 91     	 * @private
 92 		 * @memberOf ConfigurationManager#
 93     	 */
 94     	//FIXME change implementation to async-loading?
 95     	//		-> would need to add init()-function, with callback and/or return Deferred.promise
 96     	function _loadConfigFile(){
 97 	        $.ajax({
 98 	    		async: false,
 99 	    		dataType: "json",
100 	    		url: constants.getConfigurationFileUrl(),
101 	    		success: function(data){
102 	    			
103 //	    			console.debug("ConfigurationManager.constructor: loaded language settings from "+constants.getConfigurationFileUrl());//debug
104 	    			
105 					if(data){
106 	    				configData = data;
107 	    			}
108 	    		},
109 	    		error: function(data){
110 	    			console.error("ConfigurationManager.constructor: failed to load configuration from '"+constants.getConfigurationFileUrl()+"'! ERROR: "+ JSON.stringify(data));
111 	    		}
112 	    	});
113     	}
114     	
115     	//immediately load the configuration:
116     	_loadConfigFile();
117         
118         /**
119          * "Normalizes" a string or an array into a path:
120          * the result is a single, flat array where each string has
121          * been separated at dots (i.e. each path component is a separate entry).
122          * 
123          * @example 
124          *   //result is ['dot', 'separated', 'list']
125          *   _getAsPath('dot.separated.list');
126          *   _getAsPath(['dot', 'separated.list']);
127          *   _getAsPath(['dot', 'separated', 'list']);
128          * 
129          * @private
130          * @param {String|Array<String>} propertyName
131          * 				resolves a dot-separated property-name into an array.
132          * 				If <code>propertyName</code> is an Array, all contained
133          * 				String entries will be resolved, if necessary
134          * 				
135 		 * @memberOf ConfigurationManager#
136          */
137         function _getAsPath(propertyName){
138         	
139         	var path = propertyName;
140         	if( ! $.isArray(path)){
141         		path = propertyName.split('.');
142         	}
143         	else {
144         		path = _toPathArray(propertyName);
145         	}
146         	
147         	return path;
148         }
149         
150         /**
151          * "Normalizes" an array of Strings by separating
152          * each String at dots and creating one single (flat) array where
153          * each path-component is a single entry.
154          * 
155          * @private
156          * @param {Array<String>} pathList
157          * 				resolves an array with paths, i.e. dot-separated property-names
158          * 				into a single, flat array where each path component is a separate Strings
159          * 
160 		 * @memberOf ConfigurationManager#
161          */
162         function _toPathArray(pathList){
163         		
164     		var entry;
165     		var increase = 0;
166     		var size = pathList.length;
167     		var tempPath;
168     		
169     		for(var i=0; i < size; ++i){
170     			
171     			entry = pathList[i];
172     			tempPath = entry.split('.');
173     			
174     			//if entry contained dot-separated path:
175     			// replace original entry with the new sub-path
176     			if(tempPath.length > 1){
177     				pathList[i] = tempPath;
178     				increase += (tempPath.length - 1);
179     			}
180     		}
181     		
182     		//if sup-paths were inserted: flatten the array
183     		if(increase > 0){
184     			
185     			//create new array that can hold all entries
186     			var newPath = new Array(size + increase);
187     			var index = 0;
188     			for(var i=0; i < size; ++i){
189 
190         			entry = pathList[i];
191     				
192         			//flatten sub-paths into the new array:
193     				if( $.isArray(entry) ){
194     					
195     	        		for(var j=0, len=entry.length; j < len; ++j){
196     	        			newPath[index++] = entry[j];
197     					}
198     				}
199     				else {
200     					//for normal entries: just insert 
201     					newPath[index++] = entry;
202     				}
203     			}
204     			
205     			pathList = newPath;
206     		}
207         	
208     		return pathList;
209         }
210     	
211         /** @lends ConfigurationManager */
212         return {
213         	
214         	// public members
215         	
216 			/**
217 			 * Returns the currently used language. 
218 			 * 
219 			 * <p>This does not return the language of the configuration, but is a
220 			 * shortcut for {@link mmir.LanguageManager#getLanguage}.
221 			 * 
222 			 * 
223 			 * @deprecated use {@link mmir.LanguageManager#getLanguage}() instead!
224 			 * 
225 			 * @requires mmir.LanguageManager
226 			 * 
227 			 * @function
228 			 * @returns {String} The currently used language
229 			 * @public
230 			 * 
231 			 * @memberOf ConfigurationManager.prototype
232 			 */
233             getLanguage: function(){
234                 return require('languageManager').getInstance().getLanguage();
235             },
236 			/**
237 			 * Sets the currently used language.
238 			 * 
239 			 * <p>This does not set the language of the configuration, but is a
240 			 * shortcut for {@link mmir.LanguageManager#setLanguage}.
241 			 * 
242 			 * 
243 			 * @deprecated use {@link mmir.LanguageManager#setLanguage}(lang) instead!
244 			 * 
245 			 * @requires mmir.LanguageManager
246 			 * 
247 			 * @function
248 			 * @param {String} lang The language which is to be used
249 			 * @public
250 			 */
251             setLanguage: function(lang){
252             	require('languageManager').getInstance().setLanguage(lang);
253             },
254 			/**
255 			 * Returns the value of a property.
256 			 *  
257 			 * @function
258 			 * @param {String} propertyName
259 			 * 					The name of the property.
260 			 * 					NOTE: If the property does not exists at the root-level,
261 			 * 						  dot-separated names will be resolved into
262 			 * 						  object-structures, e.g.
263 			 * 						  <code>some.property</code> will be resolved
264 			 * 						  so that the <code>value</code> at:
265 			 * 						  <code>some: {property: <value>}</code>
266 			 * 						  will be returned
267 			 * @param {Boolean} [useSafeAccess] OPTIONAL
268 			 * 					if <code>true</code>, resolution of dot-separated paths
269 			 * 					will be done "safely", i.e. if a path-element does not
270 			 * 					exists, no <code>error</code> will be thrown, but instead
271 			 * 					the function will return the <code>defaultValue</code>
272 			 * 					(which will be <code>undefined</code> if the argument is not given).
273 			 * @param {any} [defaultValue] OPTIONAL
274 			 * 					a default value that will be returned, in case there is no property
275 			 * 					<code>propertyName</code>.
276 			 * 
277              * 					NOTE: if this argument is used, <code>useSafeAccess</code> must also be given!
278 			 * 
279 			 * @returns {any} 
280 			 * 					The value of the property
281 			 * @public
282 			 */
283             get: function(propertyName, useSafeAccess, defaultValue){
284             	
285             	if(configData){
286             		
287             		if(typeof configData[propertyName] !== 'undefined'){
288             			return configData[propertyName];//////////// EARLY EXIT ///////////////////
289             		}
290             		
291             		var path = _getAsPath(propertyName);
292             		
293             		//ASSERT path.length == 1: already handled by if(configData[propertyName]...
294             		
295             		if(path.length > 1){
296             			
297             			if(useSafeAccess && typeof configData[ path[0] ] === 'undefined'){
298             				return defaultValue;///////////// EARLY EXIT /////////////////////////
299             			}
300             			
301                 		var obj = configData;
302                 		var prop;
303                 		while(path.length > 1){
304                 			prop = path.shift();
305                 			obj = obj[prop];
306                 			
307                 			if(useSafeAccess && typeof obj === 'undefined'){
308                 				return defaultValue;///////////// EARLY EXIT /////////////////////
309                 			}
310                 		}
311                 		
312                 		//ASSERT now: path.length === 1
313                 		
314                 		if(typeof obj[path[0]] === 'undefined'){
315                 			return defaultValue;///////////// EARLY EXIT /////////////////////
316                 		}
317                 		return obj[path[0]];
318                 		
319                 	}
320             	}
321             	
322             	return defaultValue;
323             },
324 			/**
325 			 * Sets a property to a given value.
326 			 *  
327 			 * @function
328 			 * @param {String|Array<String>} propertyName
329 			 * 				
330 			 * 				The name of the property.
331 			 * 				
332 			 * 				If <code>propertyName</code> is an Array, it
333 			 * 				will be treated as if its entries were path-elements
334 			 * 				analogous to a dot-separated String propertyName.
335 			 * 
336 			 * 				NOTE: dot-separated names will be resolved into
337 			 * 					  object-structures, e.g.
338 			 * 					  <code>some.property</code> will be resolved
339 			 * 					  so that the <code>value</code> will set to:
340 			 * 					  <code>some: {property: <value>}</code>
341 			 * 					
342 			 * @param {any} value
343 			 * 				The value of the property
344 			 * 
345 			 * @throws {Error} if the propertyName is dot-separated AND
346 			 * 					one of its path-elements (except for the last)
347 			 * 					already exists AND its type is not 'object' 
348 			 * 
349 			 * @public
350 			 */
351             set: function(propertyName, value){
352             	if(!configData){
353             		configData = {};
354             	}
355             	
356             	var path = _getAsPath(propertyName);
357             	
358             	if(path.length > 1){
359             		var obj = configData;
360             		var prop;
361             		while(path.length > 1){
362             			
363             			prop = path.shift();
364             			
365             			if(typeof obj[prop] === 'undefined' || obj[prop] === null){
366             				obj[prop] = {};
367             			}
368             			else if(typeof obj[prop] !== 'object'){
369             				throw new Error('Cannot expand path "'+propertyName+'": path-element "'+prop+'" already exist and has type "'+(typeof obj[prop])+'"');
370             			}
371             			
372             			obj = obj[prop];
373             		}
374             		
375             		//ASSERT path.length == 1
376             		
377             		obj[path[0]] = value;
378             	}
379             	else {            	
380             		configData[propertyName] = value;
381             	}
382             },
383             
384             /**
385              * Uses {@link #get}.
386              * 
387              * If the propertyName does not exists, returns <code>undefined</code>,
388              * otherwise values will be converted into Boolean values.
389              * 
390              * Special case for Strings:
391              * the String <code>"false"</code> will be converted to
392              * Boolean value <code>false</code>.
393              * 
394              * @public
395              * @param {any} [defaultValue] OPTIONAL
396              * 
397              * 			if a default value is specified and there exists
398              * 			no property <code>propertyName</code>, the
399              * 			specified default value will be returned.
400              * 
401              * 			NOTE: if this argument is used, <code>useSafeAccess</code> must also be given!
402              * 			
403              * 			NOTE: the default value will also be converted
404              * 				  to a Boolean value, if necessary. 
405              * 
406              * @see {@link #get}
407              */
408             getBoolean: function(propertyName, useSafeAccess, defaultValue){
409             	
410             	var val = this.get(propertyName, useSafeAccess);
411             	
412             	if(typeof val === 'undefined' && typeof defaultValue !== 'undefined' ){
413             		val = defaultValue;
414             	}
415             	
416             	if(typeof val !== 'undefined'){
417             		
418             		if( val === 'false'){
419             			return false;
420             		}
421             		else {
422             			return val? true : false;
423             		}
424             	}
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 {@link #get}
449              */
450             getString: function(propertyName, useSafeAccess, defaultValue){
451             	
452             	var val = this.get(propertyName, useSafeAccess);
453             	
454             	if(typeof val === 'undefined' && typeof defaultValue !== 'undefined' ){
455             		val = defaultValue;
456             	}
457             	
458             	if(typeof val !== 'undefined'){
459             		
460             		if(typeof val === 'string'){
461             			return val;
462             		}
463             		else {
464             			return JSON.stringify(val);
465             		}
466             	}
467             	
468             }
469             
470         };//END: return {...
471         
472     }//END: construcor = function(){...
473 
474 		    	
475 	instance = new constructor();
476 	
477 	/**
478 	 * @deprecated instead: use mmir.ConfigurationManager directly
479 	 * 
480 	 * @function
481 	 * @name getInstance
482 	 * @public
483 	 * @memberOf ConfigurationManager#
484 	 */
485 	instance.getInstance = function(){
486 		return instance;
487 	};
488 	
489 	return instance;
490 });
491