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 
 29 
 30 
 31 define(['dictionary', 'controller', 'constants', 'commonUtils', 'jquery' ],
 32 
 33 	/**
 34 	 * A class for managing the controllers of the application. <br>
 35 	 * It's purpose is to load the controllers and their views / partials and provide functions to find controllers or
 36 	 * perform actions or helper-actions.
 37 	 * 
 38 	 * This "class" is structured as a singleton - so that only one instance is in use.<br>
 39 	 * 
 40 	 * 
 41 	 * @class
 42 	 * @name mmir.ControllerManager
 43 	 * @static
 44 	 * 
 45 	 * @requires jQuery.Deferred
 46 	 */
 47 	function( 
 48 		Dictionary, Controller, constants, commonUtils, $
 49 ){
 50 	//the next comment enables JSDoc2 to map all functions etc. to the correct class description
 51 	/** @scope mmir.ControllerManager.prototype */
 52 	
 53 	// private members
 54 	/**
 55 	 * Array of controller-instances
 56 	 * 
 57 	 * @type Dictionary
 58 	 * @private
 59 	 * 
 60 	 * @memberOf mmir.ControllerManager#
 61 	 */
 62 	var controllers = new Dictionary();
 63 	
 64 	/**
 65 	 * Initialize ControllerManager:
 66 	 * 
 67 	 * Load all Controllers from /controller
 68 	 * that are specified in /config/directories.json
 69 	 * 
 70 	 * @function
 71 	 * @param {Function} [callback] OPTIONAL
 72 	 * 				an optional callback that will be triggered after the controllers where loaded
 73 	 * @returns {Promise}
 74 	 * 				a Deferred.promise that will get fulfilled when controllers are loaded
 75 	 * @private
 76 	 * 
 77 	 * @memberOf mmir.ControllerManager#
 78 	 */
 79 	function _init(callback) {
 80 
 81 //		delete _instance.create;
 82 		//replace create-method with instance-getter:
 83 		_instance.create = _instance.getInstance;
 84 		
 85 		//create return value
 86 		var deferred = $.Deferred();
 87 		if(callback){
 88 			deferred.always(callback);
 89 		}
 90 		
 91 		
 92 		/**
 93 		 * HELPER FUNC: remove file extension from file-name
 94 		 * @private
 95 		 * 
 96 		 * @memberOf mmir.ControllerManager#
 97 		 */
 98 		function removeFileExt(fileName){
 99 	    	return fileName.replace(/\.[^.]+$/g,'');
100 	    }
101 		/**
102 		 * HELPER FUNC: convert first letter to upper case
103 		 * @private
104 		 * 
105 		 * @memberOf mmir.ControllerManager#
106 		 */
107 	    function firstToUpperCase(name){
108 	    	return name[0].toUpperCase()+name.substr(1);
109 	    }
110 	    
111 	    /**
112 		 * HELPER FUNC: add file path for generated / compiled view-element
113 		 *              if it exists.
114 		 * 
115 		 * @param {String} genDirPath
116 		 * 				path the the directory, where the file for the generated view-element
117 		 * 				is potentially located (generated file may not exists)
118 		 * 
119 		 * @param {PlainObject} infoObj
120 		 * 				the info-object for the view-element. MUST HAVE property <code>name</code>!
121 		 * 
122 		 * @param {String} [fileNamePrefix] OPTIONAL
123 		 * 				prefix for the file-name, e.g. in case of Partials, the file-name would be:
124 		 * 				<code>fileNamePrefix + infoObj.name</code> (+ file-extension)
125 		 * 
126 		 * @returns {PlainObject}
127 		 * 				the info-object: if a path for the generated file exists,
128 		 * 				a property <code>genPath</code> (String) with the path as value is added.
129 		 * @private
130 		 * 
131 		 * @memberOf mmir.ControllerManager#
132 		 */
133 	    function addGenPath(genDirPath, infoObj, fileNamePrefix){
134 	    	
135 	    	var prefix = fileNamePrefix? fileNamePrefix : '';
136 	    	var genPath = commonUtils.getDirectoryContentsWithFilter(genDirPath, prefix + infoObj.name+".js");
137     		if(genPath && genPath.length > 0){
138     			infoObj.genPath = genDirPath + '/' + genPath[0];
139     		}
140     		
141     		return infoObj;
142 	    }
143 		
144 		/**
145 		 * This function gets the controller file names and builds a JSON object containing information about
146 		 * the location, file name etc. for the controller itself, its views, partials, layout, and helper.
147 		 * 
148 		 * @function
149 		 * @param {String} controllerName
150 		 * 					the name of the Controller (must start with an upper case letter).
151 		 * @param {String} controllerPath
152 		 * 					the path (URL) where the file with the Controller's implementation
153 		 * 					is located (according to information in file <code>/config/directories.json</code>,
154 		 * 					i.e. {@link mmir.CommonUtils#getDirectoryStructure})
155 		 * @returns {JSON} JSON-Object containing information about the controller,
156 		 * 				its views, partials, and paths etc. 
157 		 * @private
158 		 * 
159 		 * @example
160 		 * //EXAMPLE for returned object:
161 		 * {
162 		 *   "fileName": "application",
163 		 *   "name": "Application",
164 		 *   "path": "controllers/application.js",
165 		 *   "views": [
166 		 *     {
167 		 *       "name": "login",
168 		 *       "path": "views/application/login.ehtml",
169 		 *       "genPath": "gen/views/application/login.js"
170 		 *     },
171 		 *     {
172 		 *       "name": "registration",
173 		 *       "path": "views/application/registration.ehtml",
174 		 *       "genPath": "gen/views/application/registration.js"
175 		 *     },
176 		 *     {
177 		 *       "name": "welcome",
178 		 *       "path": "views/application/welcome.ehtml",
179 		 *       "genPath": "gen/views/application/welcome.js"
180 		 *     }
181 		 *   ],
182 		 *   "partials": [
183 		 *     {
184 		 *       "name": "languageMenu",
185 		 *       "path": "views/application/~languageMenu.ehtml",
186 		 *       "genPath": "gen/views/application/~languageMenu.js"
187 		 *     }
188 		 *   ],
189 		 *   "helper": {
190 		 *     "fileName": "applicationHelper",
191 		 *     "name": "ApplicationHelper",
192 		 *     "path": "helpers/applicationHelper.js"
193 		 *   },
194 		 *   "layout": {
195 		 *     "fileName": "application",
196 		 *     "name": "application",
197 		 *     "path": "views/layouts/application.ehtml",
198 		 *     "genPath": "gen/views/layouts/application.js"
199 		 *   }
200 		 * }
201 		 * //NOTE 1: genPath is an optional field, i.e. it will only be added
202 		 *           if the corresponding file exists
203 		 * //NOTE 2: layout may be NULL
204 		 * 
205 		 * @requires mmir.CommonUtils
206 		 * @requires mmir.Constants
207 		 * 
208 		 * @memberOf mmir.ControllerManager#
209 		 */
210 	    function getControllerResources(controllerName, controllerPath){
211 	    	
212 	    	var partialsPrefix = commonUtils.getPartialsPrefix();
213 	    	var controllerFilePath = controllerPath + controllerName;
214 	    	
215 	    	var rawControllerName= removeFileExt(controllerName);
216 	    	controllerName = rawControllerName;
217 	    	
218 	    	
219 	    	var viewsPath = constants.getViewPath() + controllerName;
220 	    	var genViewsPath = constants.getCompiledViewPath() + controllerName;
221 	    	
222 	    	controllerName = firstToUpperCase(controllerName);
223 	    	
224 	    	var viewsFileList = commonUtils.getDirectoryContentsWithFilter(viewsPath, "(?!"+partialsPrefix+")*.ehtml");
225 
226 	    	var i, size;
227 	    	var viewsList = [];
228 	    	if(viewsFileList != null){
229 	    		for (i=0, size = viewsFileList.length; i < size; ++i){
230 	    			
231 	    			viewsList.push(addGenPath( genViewsPath, {
232 		    			name: removeFileExt(viewsFileList[i]),
233 		    			path: viewsPath+"/"+viewsFileList[i]
234 		    		}));
235 		    	}
236 	    	}
237 
238 	    	var partialsFileList = commonUtils.getDirectoryContentsWithFilter(viewsPath, partialsPrefix+"*.ehtml");
239 
240 	    	var partialsInfoList = [];
241 	    	if(partialsFileList != null) {
242 	    		for (i=0, size = partialsFileList.length; i < size; ++i){
243 	    		
244 		    		partialsInfoList.push(addGenPath(genViewsPath, {
245 				    		// remove leading "~" indicating it is a partial
246 				    		name: removeFileExt( partialsFileList[i].replace(partialsPrefix,'') ),
247 				        	path: viewsPath+"/"+partialsFileList[i]
248 		    		
249 					}, partialsPrefix));
250 		        }
251 	    	}
252 
253 	    	var helpersPath = constants.getHelperPath();
254 	    	helpersPath = helpersPath.substring(0, helpersPath.length-1);//remove trailing slash
255 	    	var helpersFileList = commonUtils.getDirectoryContentsWithFilter(helpersPath, "(?!"+partialsPrefix+")*.js");
256 
257 	    	var helperSuffix = constants.getHelperSuffix();
258 	    	var helperInfo = null;
259 	    	if(helpersFileList != null){
260 	    		
261 	    		for(i=0, size = helpersFileList.length; i < size; ++i){
262 		    		if(helpersFileList[i].startsWith(controllerName, true) && helpersFileList[i].endsWith(helperSuffix+'.js', true)){
263 		    	    	
264 		    			var name = removeFileExt(helpersFileList[i]);
265 		    			helperInfo = {
266 		    	    			fileName: name,
267 		    	    			name: firstToUpperCase(name),
268 		    	    			path: helpersPath+"/"+helpersFileList[i]
269 		    	    	};
270 		    		}
271 		    	}
272 	    		
273 	        }
274 	    	
275 	    	var layoutsPath = constants.getLayoutPath();
276 	    	layoutsPath = layoutsPath.substring(0, layoutsPath.length-1);//remove trailing slash
277 	    	var layoutsFileList = commonUtils.getDirectoryContentsWithFilter(layoutsPath, "(?!"+partialsPrefix+")*.ehtml");
278 	    	
279 	    	var layoutInfo = null, layoutGenPath;
280 	    	for(i=0, size = layoutsFileList.length; i < size; ++i){
281 	    		
282 	    		if( layoutsFileList[i].startsWith(controllerName, true) ){
283 	    			
284 	    			var layoutName = removeFileExt(layoutsFileList[i]);
285 	    	    	layoutInfo = {
286 			    		fileName: layoutName,
287 			    		name: firstToUpperCase(layoutName),
288 			        	path: layoutsPath+"/"+layoutsFileList[i],
289 	    	    	};
290 	    	    	
291 	    	    	layoutGenPath = constants.getCompiledLayoutPath();
292 	    	    	addGenPath(layoutGenPath.substring(0, layoutGenPath.length-1), layoutInfo);
293 		        	
294 		        	break;
295 	    		}
296 	        }
297 	    	
298 	    	var ctrlInfo = {
299 	    		fileName: rawControllerName,
300 	    		name:     controllerName,
301 	    		path:     controllerFilePath,
302 	    		
303 	    		views:    viewsList,
304 	    		partials: partialsInfoList,
305 	    		helper:   helperInfo,
306 	    		layout:   layoutInfo
307 	    	};
308 	    	
309 	    	//TEST compare info with "reference" result from original impl.:
310 //	    	var test ={
311 //	    			application: '{"fileName":"application","name":"Application","path":"controllers/application.js","views":[{"name":"login","path":"views/application/login.ehtml"},{"name":"registration","path":"views/application/registration.ehtml"},{"name":"welcome","path":"views/application/welcome.ehtml"}],"partials":[{"name":"languageMenu","path":"views/application/~languageMenu.ehtml"}],"helper":{"fileName":"applicationHelper","name":"ApplicationHelper","path":"helpers/applicationHelper.js"},"layout":{"fileName":"application","name":"application","path":"views/layouts/application.ehtml"}}',
312 //	    			calendar: '{"fileName":"calendar","name":"Calendar","path":"controllers/calendar.js","views":[{"name":"create_appointment","path":"views/calendar/create_appointment.ehtml"}],"partials":[],"helper":null,"layout":null}'
313 //	    	};
314 //	    	
315 //	    	var isEqual = (JSON.stringify(ctrlInfo) === test[ctrlInfo.fileName]);
316 //	    	console[isEqual? 'info':'error']('compliance-test: isEual? '+  isEqual);
317 	        
318 	        return ctrlInfo;
319 		};
320 
321 		commonUtils.loadImpl(
322 
323 
324 				constants.getControllerPath(),
325 
326 				false,
327 
328 				function () {
329 					
330 					console.info( '[loadControllers] done' );
331 					
332 					deferred.resolve(_instance);
333 				},
334 
335 				function isAlreadyLoaded (name) {
336 					return false; //(_instance && _instance.getController(name));
337 				},
338 
339 				function callbackStatus(status, fileName, msg) {
340 					if(status==='info'){
341 						
342 						console.info('[loadController] "'+fileName);
343 
344 						var ctrlInfo = getControllerResources(fileName, constants.getControllerPath());
345 
346 						var controller = new Controller(ctrlInfo.name, ctrlInfo);
347 
348 						if(ctrlInfo.helper){
349 							var helperPath = ctrlInfo.helper.path;
350 							var helperName = ctrlInfo.helper.name;
351 							controller.loadHelper(helperName,helperPath);
352 						}
353 
354 						controllers.put(controller.getName(), controller);
355 					}
356 					else if(status==='warning'){
357 						console.warn('[loadController] "'+fileName+'": '+msg);
358 					}
359 					else if(status==='error'){
360 						console.error('[loadController] "'+fileName+'": '+msg);
361 					}
362 					else{
363 						console.error('[loadController] '+status+' (UNKNOWN STATUS) -> "'+fileName+'": '+msg);
364 					}               
365 				}
366 
367 		);		
368 
369 		return deferred.promise(_instance);
370 
371 	};
372 
373 	/**
374      * Object containing the instance of the class {@link mmir.ControllerManager} 
375      * 
376      * @type Object
377      * @private
378 	 * @augments mmir.ControllerManager
379 	 * @ignore
380      */
381 	var _instance = {
382 			/** @scope mmir.ControllerManager.prototype *///for jsdoc2
383 
384 			/**
385 			 * Get instance of ControllerManager.
386 			 * 
387 			 * @deprecated use directly: instead of <code>mmir.ControllerManager.getInstance()</code> use <code>mmir.ControllerManager</code>
388 			 * 
389 			 * NOTE: The ControllerManager must be initialized, before it can be used! (see {@link ControllerManager#init})
390 			 * 
391 			 * @memberOf mmir.ControllerManager.prototype
392 			 */
393 			getInstance : function () {
394 
395 				return this;
396 			},	
397 
398 			// public members           
399 
400 			/**
401 			 * This function gets the controller by name. 
402 			 * 
403 			 * @function
404 			 * @param {String} ctrlName Name of the controller which should be returned
405 			 * @returns {Object} controller if found, null else
406 			 * @public
407 			 */
408 			getController: function(ctrlName){
409 				var ctrl = controllers.get(ctrlName);
410 				if(!ctrl){
411 					return null;
412 				}
413 				return ctrl;
414 			},
415 
416 
417 			/**
418 			 * This function returns names of all loaded controllers. 
419 			 * 
420 			 * @function
421 			 * @returns {Array<String>} Names of all loaded controllers
422 			 * @public
423 			 */
424 			getControllerNames: function(){
425 
426 				return controllers.getKeys();
427 			},
428 
429 
430 			/**
431 			 * This function performs an action of a controller. 
432 			 * 
433 			 * @function
434 			 * @param {String} ctrlName Name of the controller to which the action belongs
435 			 * @param {String} actionName Name of the action that should be performed
436 			 * @param {Object} data optional data that can be submitted to the action
437 			 * @returns {Object} the return object of the performed action
438 			 * @public
439 			 */
440 			perform: function(ctrlName, actionName, data){
441 				var ctrl = this.getController(ctrlName);
442 				if (ctrl != null) {
443 					return ctrl.perform(actionName, data);
444 				}
445 				else {
446 					console.error('ControllerManager.perform: the controller could not be found "'+ctrlName+'"');
447 				}
448 			},
449 
450 
451 			/**
452 			 * This function performs an action of a helper-class for a controller. 
453 			 * 
454 			 * @function
455 			 * @param {String} ctrlName Name of the controller to which the helper action belongs
456 			 * @param {String} actionName Name of the action that should be performed by the helper
457 			 * @param {Object} data optional data that can be submitted to the action
458 			 * @returns {Object} the return object of the performed action
459 			 * @public
460 			 */
461 			performHelper: function(ctrlName, actionName, data) {
462 
463 				var ctrl = this.getController(ctrlName);
464 				if (ctrl != null) {
465 					if(arguments.length > 3){
466 						return ctrl.performHelper(actionName, data, arguments[3]);
467 					}
468 					else {
469 						return ctrl.performHelper(actionName, data);
470 					}
471 				}
472 				else {
473 					console.error('ControllerManager.performHelper: the controller could not be found "'+ctrlName+'"');
474 				}
475 			},
476 			/**
477 			 * This function must be called before using the {@link mmir.ControllerManager}. The Initialization process is asynchronous, 
478 			 * because javascript-files must be loaded (the controllers).
479 			 * To ensure that the ControllerManager is initialized, a callback can be used, or the returned
480 			 * <em>Promise</em> (e.g. see documentation of jQuery.Deferred) for code, that relies
481 			 * on the presence of the loaded controllers.<br>   
482 			 * 
483 			 * 
484 			 * <div class="box important">
485 			 * <b>Note:</b>
486 			 * The callback function should be used for code, that requires the prior loading of the controllers.<br> 
487 			 * The callback mechanism is necessary, because loading the controllers is asynchronous.<br><br>
488 			 * If provided, the callback function is invoked with 1 argument, the ControllerManager instance:<br>
489 			 * <code> callbackFunction(controllerManagerInstance) </code>
490 			 * </div>
491 			 * 
492 			 * @function
493 			 * 
494 			 * @param {Function} [callback] OPTIONAL
495 			 * 				an optional callback that will be triggered after the controllers where loaded
496 			 * @returns {Promise}
497 			 * 				a Deferred.promise that will get fulfilled when controllers are loaded
498 			 * @example
499 			 *  //recommended style:
500 			 *  require(['controllerManager', ...], function(controllerManager, ...) {
501 			 *  	controllerManager.init().then(function(theInitializedControllerInstance){
502 			 *  		...
503 			 *  	});
504 			 *  })
505 			 *  
506 			 *  //old style:
507 			 * 	function afterLoadingControllers(controllerManagerInstance){
508 			 * 		var appCtrl = controllerManagerInstance.getController('Application');
509 			 * 		//do something...
510 			 * 	} 
511 			 * 	mmir.ControllerManager.init(afterLoadingControllers);
512 			 * @public
513 			 */
514 			init: _init
515 
516 	};
517 	/**@ignore*/
518 	return _instance;
519 	
520 });
521 
522 
523