Source: manager/controllerManager.js

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