Source: manager/modelManager.js

/*
 * 	Copyright (C) 2012-2013 DFKI GmbH
 * 	Deutsches Forschungszentrum fuer Kuenstliche Intelligenz
 * 	German Research Center for Artificial Intelligence
 * 	http://www.dfki.de
 * 
 * 	Permission is hereby granted, free of charge, to any person obtaining a 
 * 	copy of this software and associated documentation files (the 
 * 	"Software"), to deal in the Software without restriction, including 
 * 	without limitation the rights to use, copy, modify, merge, publish, 
 * 	distribute, sublicense, and/or sell copies of the Software, and to 
 * 	permit persons to whom the Software is furnished to do so, subject to 
 * 	the following conditions:
 * 
 * 	The above copyright notice and this permission notice shall be included 
 * 	in all copies or substantial portions of the Software.
 * 
 * 	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 * 	OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 * 	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * 	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 * 	CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 * 	TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * 	SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */



define( [ 'mmirf/dictionary', 'mmirf/constants', 'mmirf/commonUtils', 'mmirf/logger', 'mmirf/util/deferred', 'module' ],
	/**
	 * 
	 * A class for managing the models of the application (MVC-Component). <br>
	 * It's purpose is to load the models automatically.
	 * 
	 * This "class" is a singleton - so that only one instance is in
	 * use.<br>
	 * 
	 * TODO add example for usage (models as "class" / models as "singleton")
	 * 
	 * @class
	 * @name ModelManager
	 * @memberOf mmir
	 * @static
	 * 
	 */
	function( 
    		Dictionary,  constants, commonUtils, Logger, deferred, module
){
	//the next comment enables JSDoc2 to map all functions etc. to the correct class description
	/** @scope mmir.ModelManager.prototype */
	
	/**
	 * @private
	 * @type Logger
	 * @memberOf mmir.ModelManager#
	 * @see mmir.Logging#create
	 */
	var logger = Logger.create(module);

	// private members
    /**
     * Array of models
     * 
     * @type Dictionary
     * @private
     * 
	 * @memberOf mmir.ModelManager#
     */
	var models = new Dictionary();
	
	/**
     * Name of the default namespace 
     * (within the global space) 
     * into which Models will be loaded
     * 
     * @constant
     * @type String
     * @private
     * 
	 * @memberOf mmir.ModelManager#
     */
	var MODEL_DEFAULT_NAMESPACE_NAME = 'mmir';
	
	/**
     * The global namespace
     * 
     * @constant
     * @type Object
     * @private
     * 
	 * @memberOf mmir.ModelManager#
     */
	var GLOBAL_NAMESPACE = typeof window !== 'undefined'? window : global;

	/**
	 * 
	 * This function returns the fully qualified model name (including namespace(s)).
	 * 
	 * @function
	 * @param {String}
	 *            modelClassName the model's class-name (i.e. without namespace)
	 * @returns {String} fully qualified name for the model
	 * @private
	 * 
	 * @memberOf mmir.ModelManager#
	 */
    function getFullModelName(modelClassName){
    	if( ! MODEL_DEFAULT_NAMESPACE_NAME){
    		return modelClassName;
    	}
    	else {
    		return MODEL_DEFAULT_NAMESPACE_NAME + '.' + modelClassName;
    	}
    }
    
    /**
	 * 
	 * This function returns all loaded models. 
	 * 
	 * @function
	 * @returns {Array<String>} all loaded model names
	 * @public
	 * @memberOf mmir.ModelManager#
	 */
    function getModelNames(){//TODO export this function on _instance?
    	return models.getKeys();
    }

	/**
	 * This function invokes the method
	 * {@link mmir.ModelManager#foundModelsCallBack} to load all
	 * models in the path specified by *modelPath*.
	 * 
	 * @function
	 * @param {Function} [initCallbackFunction] OPTIONAL
	 *            The callback function from the constructor
	 *            which shall be called after the initialization of the
	 *            {@link mmir.ModelManager}.
	 *            
	 * @param {Object} [ctx] OPTIONAL
	 * 				the context for the model implementations (DEFAULT: the global context, i.e. window)
	 * @returns {Promise} 
	 * 					a deferred promise that gets fulfilled when models are loaded.
	 * @private
	 * @memberOf mmir.ModelManager#
	 */
	function _init(initCallbackFunction, ctx) {
		
		/** @scope mmir.ModelManager.prototype */
		
		//shift arguments if necessary:
		//shift arguments if necessary:
		if(!ctx && typeof initCallbackFunction !== 'function'){
			ctx = initCallbackFunction;
			initCallbackFunction = void(0);
		}

		/**
		 * 
		 * This function returns the fully qualified model name (incl. namespace(s)). 
		 * 
		 * @function
		 * @name getModelByName
		 * @param {String|Array<String>} fullModelName the fully qualified model name (i.e. with namespace(s))
		 * 								Note, if {String} components/namespaces are separated by a <tt>.</tt> (dot)
		 * 								If {Array<String>} the entries correspond to the namespace components (without dots),
		 * 								  where the last entry corresponds to the class/singleton name
		 * @param {Object} [ctx] OPTIONAL
		 * 				the (base) context for the model implementations (DEFAULT: the global context GLOBAL_NAMESPACE)
		 * @returns {Object} the "raw" model object (may be a constructor or the main-singleton-namespace).
		 * 					 Or <tt>null</tt> if there is no Model with the name.
		 * @private
		 * 
		 * @requires mmir.CommonUtils#isArray
		 * 
		 * @see mmir.ModelManager#getFullModelName
		 * @see mmir.ModelManager#doGetModelInstance
		 * 
		 * @memberOf mmir.ModelManager#
		 */
        function getModelByName(fullModelName, ctx){
        	
        	ctx = ctx || GLOBAL_NAMESPACE;
        	
        	var components;
        	if(commonUtils.isArray(fullModelName)){
        		components = fullModelName;
        	}
        	else {
        		components = fullModelName.split('.');
        	}
        	
        	//try to resolve fully-qualified name (without triggering an error)
        	var currentNameSpace = ctx;
        	for(var i=0, size = components.length; i < size; ++i){
        		currentNameSpace = currentNameSpace[components[i]];
    			if(typeof currentNameSpace !== 'undefined' ){
            		if(i === size-1){
            			return currentNameSpace;
            		}
    			}
    			else {
    				break;
    			}
        	}
        	
        	if(size > 0 && ctx[components[size-1]]){
        		//try simple model name
        		return ctx[components[size-1]];
        	}
        	
        	var isReTry = ctx !== GLOBAL_NAMESPACE;
        	
        	var logFunc = isReTry? 'info' : 'error';
        	logger[logFunc]('ModelManager.getModelByName: could not find model'
        			+(isReTry? ' in model context with path "<modelContext>.' : ' "')
					+(components.join('.'))
					+'": '
					+(isReTry? 'there is no' : 'invalid')
					+' namespace/class: "'
					+components[i]
					+'"' 
					+(isReTry? ', trying to find model in global namespace ...' : '')
			);
        	
        	return null;
        }
        
        /**
         * Returns the instance for a model implementation:
         * 
         * If the model-object is a constructor (i.e. a function),
         * a new instance is created and returned.
         * 
         * Otherwise the model-object itself is returned (e.g. for 
         * singleton pattern models).
         * 
         * @function
         * @private
         * 
		 * @see mmir.ModelManager#getModelByName
		 * 
		 * @memberOf mmir.ModelManager#
         */
        function doGetModelInstance(modelImplObject){
        	
        	
        	if(typeof modelImplObject === 'function'){
        		return new modelImplObject();
        	}
        	//TODO allow alternative initialization methods for models?:
//        	else if(typeof modelImplObject.getInstance === 'function'){
//        		return modelImplObject.getInstance();
//        	}
//        	else if(typeof modelImplObject.init === 'function'){
//        		return modelImplObject.init();
//        	}
        	else{
        		return modelImplObject;
        	}
        	
        	//TODO export to requirejs?
        	//define(modelImplObject.toString(), function(){ return THE_MODEL_INSTANCE;});
        	
        }

		var _defer = deferred();
		if(initCallbackFunction){
			_defer.then(initCallbackFunction, initCallbackFunction);
		}

		commonUtils.loadImpl(

				constants.getModelPath(), 

				false, 

				function () {
					
					logger.debug('[loadModels] done');

					_defer.resolve(_instance);
				},

				function isAlreadyLoaded(name) {
					return false; // ( _instance && _instance.getModel(name) ); TODO
				}, 

				function callbackStatus(status, fileName, msg) {
					
					if (status === 'info') {
						
						logger.info('[loadModel] "' + fileName);
						
						var modelName = fileName.charAt(0).toUpperCase() + fileName.slice(1).replace(/\.[^.]+$/g, "");
						var fullName = getFullModelName(modelName);
						var modelImpl = getModelByName(fullName, ctx);
						if(!modelImpl){
							modelImpl = getModelByName(fullName);
						}
						var modelInstance;
						if (modelImpl) {
							modelInstance = doGetModelInstance(modelImpl);
						} else {
							logger.error('ModelManager.load: Could not find implementation for Model "' + modelName + '" (' + fullName + ') for file ' + fileName);
							modelInstance = modelName;
						}
						models.put(fullName, modelInstance);
						
					}
					else if (status === 'warning') {
						logger.warn('[loadModel] "' + fileName + '": ' + msg);
					}
					else if (status === 'error') {
						logger.error('[loadModel] "' + fileName + '": ' + msg);
					}
					else {
						logger.error('[loadModel] ' + status + ' (UNKNOWN STATUS) -> "' + fileName + '": ' + msg);
					}
				}

//				, function callbackAfterLoading(jsfile) {
//					var modelName = jsfile.charAt(0).toUpperCase() + jsfile.slice(1).replace(/\.[^.]+$/g, "");
//					var fullName = getFullModelName(modelName);
//					var modelImpl = getModelByName(fullName);
//					var modelInstance;
//					if (modelImpl) {
//						modelInstance = doGetModelInstance(modelImpl);
//					} else {
//						logger.error('ModelManager.load: Could not find implementation for Model "' + modelName + '" (' + fullName + ') for file ' + jsfile);
//						modelInstance = modelName;
//					}
//					models.put(fullName, modelInstance);
//				}
		);


		return _defer;
	};

	/**
	 * Object containing the instance of the class {@link mmir.ModelManager}
	 * 
	 * @type Object
	 * @private
	 * @ignore
	 */
	var _instance = {
			/** @scope mmir.ModelManager.prototype */

			// public members
			/**
			 * This function gets the model by name.
			 * 
			 * @function
			 * @param {String}
			 *            modelName Name of the model which should be returned
			 * @returns {Object} The model if found, null else
			 * @public
			 * @memberOf mmir.ModelManager.prototype
			 */
			get: function(modelName) {
				var retModel = null;

				// TODO implement mechanism for multiple/configurable model namespaces
				// (add optional namespace argument to getModel)
				var fullModelName = getFullModelName(modelName);

				retModel = models.get(fullModelName);
				if (!retModel) {
					logger.error('Could not find Model "' + modelName + '" at ' + fullModelName);
					return null;
				}
				return retModel;
			},


			/**
			 * This function returns all loaded model names.
			 * 
			 * @function
			 * @returns {Array<string>} All loaded model names
			 * @public
			 */
			getNames: function() {
				return models.getKeys();
			},

			/**
			 * This function must be called before using the {@link mmir.ModelManager}. The Initialization process is asynchronous, 
			 * because javascript-files must be loaded (the models), so it forces a synchronous behavior by using
			 * a callback function containing the instructions, which rely on the presence of the loaded models.<br>   
			 * 
			 * It loads the models and then calls the callback functions and returns the instance of this class.
			 * 
			 * <div class="box important">
			 * <b>Note:</b>
			 * The callback function should contain all (!) instructions which require the prior loading of the models.<br> 
			 * The callback mechanism is necessary, because loading the models is asynchronous.<br><br>
			 * If provided, the callback function is invoked with 1 argument, the ModelManager instance:<br>
			 * <code> callbackFunction(modelManagerInstance) </code>
			 * </div>
			 * 
			 * NOTE: use EITHER callback-function OR returned Promise -- do not use both!
			 * 
			 * @function
			 * @param {Function} [callbackFunction] 
			 * 					The function which should be called after loading all controllers
			 * @param {Object} [ctx] OPTIONAL
			 * 				the context for the model implementations (DEFAULT: the global context, i.e. window)
			 * @returns {Promise} 
			 * 					a deferred promise that gets fulfilled when models are loaded.
			 * @example
			 * 	function afterLoadingModels(modelManagerInstance){
			 * 		var userModel = modelManagerInstance.get('User');
			 * 		//do something...
			 * 	} 
			 * 	mmir.model.create(afterLoadingModels);
			 * @public
			 */
			init: _init

	};

	return _instance;
	
});