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