<?php
/**
 * TApplication class file
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System
 */

/**
 * Includes core interfaces essential for TApplication class
 */
require_once(PRADO_DIR.'/interfaces.php');

/**
 * Includes core classes essential for TApplication class
 */
Prado::using('System.TApplicationComponent');
Prado::using('System.TModule');
Prado::using('System.TService');
Prado::using('System.Exceptions.TErrorHandler');
Prado::using('System.Caching.TCache');
Prado::using('System.IO.TTextWriter');
Prado::using('System.Collections.TList');
Prado::using('System.Collections.TMap');
Prado::using('System.Collections.TStack');
Prado::using('System.Xml.TXmlDocument');
Prado::using('System.Security.TAuthorizationRule');
Prado::using('System.Security.TSecurityManager');
Prado::using('System.Web.THttpUtility');
Prado::using('System.Web.Javascripts.TJavaScript');
Prado::using('System.Web.THttpRequest');
Prado::using('System.Web.THttpResponse');
Prado::using('System.Web.THttpSession');
Prado::using('System.Web.Services.TPageService');
Prado::using('System.Web.TAssetManager');
Prado::using('System.I18N.TGlobalization');

/**
 * TApplication class.
 *
 * TApplication coordinates modules and services, and serves as a configuration
 * context for all Prado components.
 *
 * TApplication uses a configuration file to specify the settings of
 * the application, the modules, the services, the parameters, and so on.
 *
 * TApplication adopts a modular structure. A TApplication instance is a composition
 * of multiple modules. A module is an instance of class implementing
 * {@link IModule} interface. Each module accomplishes certain functionalities
 * that are shared by all Prado components in an application.
 * There are default modules and user-defined modules. The latter offers extreme
 * flexibility of extending TApplication in a plug-and-play fashion.
 * Modules cooperate with each other to serve a user request by following
 * a sequence of lifecycles predefined in TApplication.
 *
 * TApplication has four modes that can be changed by setting {@link setMode Mode}
 * property (in the application configuration file).
 * - <b>Off</b> mode will prevent the application from serving user requests.
 * - <b>Debug</b> mode is mainly used during application development. It ensures
 *   the cache is always up-to-date if caching is enabled. It also allows
 *   exceptions are displayed with rich context information if they occur.
 * - <b>Normal</b> mode is mainly used during production stage. Exception information
 *   will only be recorded in system error logs. The cache is ensured to be
 *   up-to-date if it is enabled.
 * - <b>Performance</b> mode is similar to <b>Normal</b> mode except that it
 *   does not ensure the cache is up-to-date.
 *
 * TApplication dispatches each user request to a particular service which
 * finishes the actual work for the request with the aid from the application
 * modules.
 *
 * TApplication maintains a lifecycle with the following stages:
 * - [construct] : construction of the application instance
 * - [initApplication] : load application configuration and instantiate modules and the requested service
 * - onBeginRequest : this event happens right after application initialization
 * - onAuthentication : this event happens when authentication is needed for the current request
 * - onAuthenticationComplete : this event happens right after the authentication is done for the current request
 * - onAuthorization : this event happens when authorization is needed for the current request
 * - onAuthorizationComplete : this event happens right after the authorization is done for the current request
 * - onLoadState : this event happens when application state needs to be loaded
 * - onLoadStateComplete : this event happens right after the application state is loaded
 * - onPreRunService : this event happens right before the requested service is to run
 * - runService : the requested service runs
 * - onSaveState : this event happens when application needs to save its state
 * - onSaveStateComplete : this event happens right after the application saves its state
 * - onPreFlushOutput : this event happens right before the application flushes output to client side.
 * - flushOutput : the application flushes output to client side.
 * - onEndRequest : this is the last stage a request is being completed
 * - [destruct] : destruction of the application instance
 * Modules and services can attach their methods to one or several of the above
 * events and do appropriate processing when the events are raised. By this way,
 * the application is able to coordinate the activities of modules and services
 * in the above order. To terminate an application before the whole lifecycle
 * completes, call {@link completeRequest}.
 *
 * Examples:
 * - Create and run a Prado application:
 * <code>
 * $application=new TApplication($configFile);
 * $application->run();
 * </code>
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TApplication extends TComponent
{
	/**
	 * possible application mode.
	 * @deprecated deprecated since version 3.0.4 (use TApplicationMode constants instead)
	 */
	const STATE_OFF='Off';
	const STATE_DEBUG='Debug';
	const STATE_NORMAL='Normal';
	const STATE_PERFORMANCE='Performance';

	/**
	 * Page service ID
	 */
	const PAGE_SERVICE_ID='page';
	/**
	 * Application configuration file name
	 */
	const CONFIG_FILE='application.xml';
	/**
	 * File extension for external config files
	 */
	const CONFIG_FILE_EXT='.xml';
	/**
	 * Runtime directory name
	 */
	const RUNTIME_PATH='runtime';
	/**
	 * Config cache file
	 */
	const CONFIGCACHE_FILE='config.cache';
	/**
	 * Global data file
	 */
	const GLOBAL_FILE='global.cache';

	/**
	 * @var array list of events that define application lifecycles
	 */
	private static $_steps=array(
		'onBeginRequest',
		'onLoadState',
		'onLoadStateComplete',
		'onAuthentication',
		'onAuthenticationComplete',
		'onAuthorization',
		'onAuthorizationComplete',
		'onPreRunService',
		'runService',
		'onSaveState',
		'onSaveStateComplete',
		'onPreFlushOutput',
		'flushOutput'
	);

	/**
	 * @var string application ID
	 */
	private $_id;
	/**
	 * @var string unique application ID
	 */
	private $_uniqueID;
	/**
	 * @var boolean whether the request is completed
	 */
	private $_requestCompleted=false;
	/**
	 * @var integer application state
	 */
	private $_step;
	/**
	 * @var array available services and their configurations indexed by service IDs
	 */
	private $_services;
	/**
	 * @var IService current service instance
	 */
	private $_service;
	/**
	 * @var array list of application modules
	 */
	private $_modules=array();
	/**
	 * @var TMap list of application parameters
	 */
	private $_parameters;
	/**
	 * @var string configuration file
	 */
	private $_configFile;
	/**
	 * @var string application base path
	 */
	private $_basePath;
	/**
	 * @var string directory storing application state
	 */
	private $_runtimePath;
	/**
	 * @var boolean if any global state is changed during the current request
	 */
	private $_stateChanged=false;
	/**
	 * @var array global variables (persistent across sessions, requests)
	 */
	private $_globals=array();
	/**
	 * @var string cache file
	 */
	private $_cacheFile;
	/**
	 * @var TErrorHandler error handler module
	 */
	private $_errorHandler;
	/**
	 * @var THttpRequest request module
	 */
	private $_request;
	/**
	 * @var THttpResponse response module
	 */
	private $_response;
	/**
	 * @var THttpSession session module, could be null
	 */
	private $_session;
	/**
	 * @var ICache cache module, could be null
	 */
	private $_cache;
	/**
	 * @var IStatePersister application state persister
	 */
	private $_statePersister;
	/**
	 * @var IUser user instance, could be null
	 */
	private $_user;
	/**
	 * @var TGlobalization module, could be null
	 */
	private $_globalization;
	/**
	 * @var TSecurityManager security manager module
	 */
	private $_security;
	/**
	 * @var TAssetManager asset manager module
	 */
	private $_assetManager;
	/**
	 * @var TAuthorizationRuleCollection collection of authorization rules
	 */
	private $_authRules;
	/**
	 * @var TApplicationMode application mode
	 */
	private $_mode=TApplicationMode::Debug;

	/**
	 * Constructor.
	 * Sets application base path and initializes the application singleton.
	 * Application base path refers to the root directory storing application
	 * data and code not directly accessible by Web users.
	 * By default, the base path is assumed to be the <b>protected</b>
	 * directory under the directory containing the current running script.
	 * @param string application base path or configuration file path.
	 *        If the parameter is a file, it is assumed to be the application
	 *        configuration file, and the directory containing the file is treated
	 *        as the application base path.
	 *        If it is a directory, it is assumed to be the application base path,
	 *        and within that directory, a file named <b>application.xml</b>
	 *        will be looked for. If found, the file is considered as the application
	 *        configuration file.
	 * @param boolean whether to cache application configuration. Defaults to true.
	 * @throws TConfigurationException if configuration file cannot be read or the runtime path is invalid.
	 */
	public function __construct($basePath='protected',$cacheConfig=true)
	{
		// register application as a singleton
		Prado::setApplication($this);

		$this->resolvePaths($basePath);

		if($cacheConfig)
			$this->_cacheFile=$this->_runtimePath.DIRECTORY_SEPARATOR.self::CONFIGCACHE_FILE;

		// generates unique ID by hashing the runtime path
		$this->_uniqueID=md5($this->_runtimePath);
		$this->_parameters=new TMap;
		$this->_services=array(self::PAGE_SERVICE_ID=>array('TPageService',array(),null));
		Prado::setPathOfAlias('Application',$this->_basePath);
	}

	/**
	 * Resolves application-relevant paths.
	 * This method is invoked by the application constructor
	 * to determine the application configuration file,
	 * application root path and the runtime path.
	 * @param string the application root path or the application configuration file
	 * @see setBasePath
	 * @see setRuntimePath
	 * @see setConfigurationFile
	 */
	protected function resolvePaths($basePath)
	{
		// determine configuration path and file
		if(empty($basePath) || ($basePath=realpath($basePath))===false)
			throw new TConfigurationException('application_basepath_invalid',$basePath);
		if(is_file($basePath.DIRECTORY_SEPARATOR.self::CONFIG_FILE))
			$configFile=$basePath.DIRECTORY_SEPARATOR.self::CONFIG_FILE;
		else if(is_file($basePath))
		{
			$configFile=$basePath;
			$basePath=dirname($configFile);
		}
		else
			$configFile=null;

		// determine runtime path
		$runtimePath=$basePath.DIRECTORY_SEPARATOR.self::RUNTIME_PATH;
		if(is_writable($runtimePath))
		{
			if($configFile!==null)
			{
				$runtimePath.=DIRECTORY_SEPARATOR.basename($configFile).'-'.Prado::getVersion();
				if(!is_dir($runtimePath))
				{
					if(@mkdir($runtimePath)===false)
						throw new TConfigurationException('application_runtimepath_failed',$runtimePath);
					@chmod($runtimePath, PRADO_CHMOD); //make it deletable
				}
				$this->setConfigurationFile($configFile);
			}
			$this->setBasePath($basePath);
			$this->setRuntimePath($runtimePath);
		}
		else
			throw new TConfigurationException('application_runtimepath_invalid',$runtimePath);

	}

	/**
	 * Executes the lifecycles of the application.
	 * This is the main entry function that leads to the running of the whole
	 * Prado application.
	 */
	public function run()
	{
		try
		{
			$this->initApplication();
			$n=count(self::$_steps);
			$this->_step=0;
			$this->_requestCompleted=false;
			while($this->_step<$n)
			{
				if($this->_mode===self::STATE_OFF)
					throw new THttpException(503,'application_unavailable');
				if($this->_requestCompleted)
					break;
				$method=self::$_steps[$this->_step];
				Prado::trace("Executing $method()",'System.TApplication');
				$this->$method();
				$this->_step++;
			}
		}
		catch(Exception $e)
		{
			$this->onError($e);
		}
		$this->onEndRequest();
	}

	/**
	 * Completes current request processing.
	 * This method can be used to exit the application lifecycles after finishing
	 * the current cycle.
	 */
	public function completeRequest()
	{
		$this->_requestCompleted=true;
	}

	/**
	 * @return boolean whether the current request is processed.
	 */
	public function getRequestCompleted()
	{
		return $this->_requestCompleted;
	}

	/**
	 * Returns a global value.
	 *
	 * A global value is one that is persistent across users sessions and requests.
	 * @param string the name of the value to be returned
	 * @param mixed the default value. If $key is not found, $defaultValue will be returned
	 * @return mixed the global value corresponding to $key
	 */
	public function getGlobalState($key,$defaultValue=null)
	{
		return isset($this->_globals[$key])?$this->_globals[$key]:$defaultValue;
	}

	/**
	 * Sets a global value.
	 *
	 * A global value is one that is persistent across users sessions and requests.
	 * Make sure that the value is serializable and unserializable.
	 * @param string the name of the value to be set
	 * @param mixed the global value to be set
	 * @param mixed the default value. If $key is not found, $defaultValue will be returned
	 */
	public function setGlobalState($key,$value,$defaultValue=null)
	{
		$this->_stateChanged=true;
		if($value===$defaultValue)
			unset($this->_globals[$key]);
		else
			$this->_globals[$key]=$value;
	}

	/**
	 * Clears a global value.
	 *
	 * The value cleared will no longer be available in this request and the following requests.
	 * @param string the name of the value to be cleared
	 */
	public function clearGlobalState($key)
	{
		$this->_stateChanged=true;
		unset($this->_globals[$key]);
	}

	/**
	 * Loads global values from persistent storage.
	 * This method is invoked when {@link onLoadState OnLoadState} event is raised.
	 * After this method, values that are stored in previous requests become
	 * available to the current request via {@link getGlobalState}.
	 */
	protected function loadGlobals()
	{
		$this->_globals=$this->getApplicationStatePersister()->load();
	}

	/**
	 * Saves global values into persistent storage.
	 * This method is invoked when {@link onSaveState OnSaveState} event is raised.
	 */
	protected function saveGlobals()
	{
		if($this->_stateChanged)
		{
			$this->_stateChanged=false;
			$this->getApplicationStatePersister()->save($this->_globals);
		}
	}

	/**
	 * @return string application ID
	 */
	public function getID()
	{
		return $this->_id;
	}

	/**
	 * @param string application ID
	 */
	public function setID($value)
	{
		$this->_id=$value;
	}

	/**
	 * @return string an ID that uniquely identifies this Prado application from the others
	 */
	public function getUniqueID()
	{
		return $this->_uniqueID;
	}

	/**
	 * @return TApplicationMode application mode. Defaults to TApplicationMode::Debug.
	 */
	public function getMode()
	{
		return $this->_mode;
	}

	/**
	 * @param TApplicationMode application mode
	 */
	public function setMode($value)
	{
		$this->_mode=TPropertyValue::ensureEnum($value,'TApplicationMode');
	}

	/**
	 * @return string the directory containing the application configuration file (absolute path)
	 */
	public function getBasePath()
	{
		return $this->_basePath;
	}

	/**
	 * @param string the directory containing the application configuration file
	 */
	public function setBasePath($value)
	{
		$this->_basePath=$value;
	}

	/**
	 * @return string the application configuration file (absolute path)
	 */
	public function getConfigurationFile()
	{
		return $this->_configFile;
	}

	/**
	 * @param string the application configuration file (absolute path)
	 */
	public function setConfigurationFile($value)
	{
		$this->_configFile=$value;
	}

	/**
	 * @return string the directory storing cache data and application-level persistent data. (absolute path)
	 */
	public function getRuntimePath()
	{
		return $this->_runtimePath;
	}

	/**
	 * @param string the directory storing cache data and application-level persistent data. (absolute path)
	 */
	public function setRuntimePath($value)
	{
		$this->_runtimePath=$value;
	}

	/**
	 * @return IService the currently requested service
	 */
	public function getService()
	{
		return $this->_service;
	}

	/**
	 * @param IService the currently requested service
	 */
	public function setService($value)
	{
		$this->_service=$value;
	}

	/**
	 * Adds a module to application.
	 * Note, this method does not do module initialization.
	 * @param string ID of the module
	 * @param IModule module object
	 */
	public function setModule($id,IModule $module)
	{
		if(isset($this->_modules[$id]))
			throw new TConfigurationException('application_moduleid_duplicated',$id);
		else
			$this->_modules[$id]=$module;
	}

	/**
	 * @return IModule the module with the specified ID, null if not found
	 */
	public function getModule($id)
	{
		return isset($this->_modules[$id])?$this->_modules[$id]:null;
	}

	/**
	 * @return array list of loaded application modules, indexed by module IDs
	 */
	public function getModules()
	{
		return $this->_modules;
	}

	/**
	 * Returns the list of application parameters.
	 * Since the parameters are returned as a {@link TMap} object, you may use
	 * the returned result to access, add or remove individual parameters.
	 * @return TMap the list of application parameters
	 */
	public function getParameters()
	{
		return $this->_parameters;
	}

	/**
	 * @return THttpRequest the request module
	 */
	public function getRequest()
	{
		if(!$this->_request)
		{
			$this->_request=new THttpRequest;
			$this->_request->init(null);
		}
		return $this->_request;
	}

	/**
	 * @param THttpRequest the request module
	 */
	public function setRequest(THttpRequest $request)
	{
		$this->_request=$request;
	}

	/**
	 * @return THttpResponse the response module
	 */
	public function getResponse()
	{
		if(!$this->_response)
		{
			$this->_response=new THttpResponse;
			$this->_response->init(null);
		}
		return $this->_response;
	}

	/**
	 * @param THttpRequest the request module
	 */
	public function setResponse(THttpResponse $response)
	{
		$this->_response=$response;
	}

	/**
	 * @return THttpSession the session module, null if session module is not installed
	 */
	public function getSession()
	{
		if(!$this->_session)
		{
			$this->_session=new THttpSession;
			$this->_session->init(null);
		}
		return $this->_session;
	}

	/**
	 * @param THttpSession the session module
	 */
	public function setSession(THttpSession $session)
	{
		$this->_session=$session;
	}

	/**
	 * @return TErrorHandler the error handler module
	 */
	public function getErrorHandler()
	{
		if(!$this->_errorHandler)
		{
			$this->_errorHandler=new TErrorHandler;
			$this->_errorHandler->init(null);
		}
		return $this->_errorHandler;
	}

	/**
	 * @param TErrorHandler the error handler module
	 */
	public function setErrorHandler(TErrorHandler $handler)
	{
		$this->_errorHandler=$handler;
	}

	/**
	 * @return TSecurityManager the security manager module
	 */
	public function getSecurityManager()
	{
		if(!$this->_security)
		{
			$this->_security=new TSecurityManager;
			$this->_security->init(null);
		}
		return $this->_security;
	}

	/**
	 * @param TSecurityManager the security manager module
	 */
	public function setSecurityManager(TSecurityManager $sm)
	{
		$this->_security=$sm;
	}

	/**
	 * @return TAssetManager asset manager
	 */
	public function getAssetManager()
	{
		if(!$this->_assetManager)
		{
			$this->_assetManager=new TAssetManager;
			$this->_assetManager->init(null);
		}
		return $this->_assetManager;
	}

	/**
	 * @param TAssetManager asset manager
	 */
	public function setAssetManager(TAssetManager $value)
	{
		$this->_assetManager=$value;
	}

	/**
	 * @return IStatePersister application state persister
	 */
	public function getApplicationStatePersister()
	{
		if(!$this->_statePersister)
		{
			$this->_statePersister=new TApplicationStatePersister;
			$this->_statePersister->init(null);
		}
		return $this->_statePersister;
	}

	/**
	 * @param IStatePersister  application state persister
	 */
	public function setApplicationStatePersister(IStatePersister $persister)
	{
		$this->_statePersister=$persister;
	}

	/**
	 * @return ICache the cache module, null if cache module is not installed
	 */
	public function getCache()
	{
		return $this->_cache;
	}

	/**
	 * @param ICache the cache module
	 */
	public function setCache(ICache $cache)
	{
		$this->_cache=$cache;
	}

	/**
	 * @return IUser the application user
	 */
	public function getUser()
	{
		return $this->_user;
	}

	/**
	 * @param IUser the application user
	 */
	public function setUser(IUser $user)
	{
		$this->_user=$user;
	}

	/**
	 * @param boolean whether to create globalization if it does not exist
	 * @return TGlobalization globalization module
	 */
	public function getGlobalization($createIfNotExists=true)
	{
		if($this->_globalization===null && $createIfNotExists)
			$this->_globalization=new TGlobalization;
		return $this->_globalization;
	}

	/**
	 * @param TGlobalization globalization module
	 */
	public function setGlobalization(TGlobalization $glob)
	{
		$this->_globalization=$glob;
	}

	/**
	 * @return TAuthorizationRuleCollection list of authorization rules for the current request
	 */
	public function getAuthorizationRules()
	{
		if($this->_authRules===null)
			$this->_authRules=new TAuthorizationRuleCollection;
		return $this->_authRules;
	}

	/**
	 * Applies an application configuration.
	 * @param TApplicationConfiguration the configuration
	 * @param boolean whether the configuration is specified within a service.
	 */
	public function applyConfiguration($config,$withinService=false)
	{
		if($config->getIsEmpty())
			return;

		// set path aliases and using namespaces
		foreach($config->getAliases() as $alias=>$path)
			Prado::setPathOfAlias($alias,$path);
		foreach($config->getUsings() as $using)
			Prado::using($using);

		// set application properties
		if(!$withinService)
		{
			foreach($config->getProperties() as $name=>$value)
				$this->setSubProperty($name,$value);
		}

		// load parameters
		foreach($config->getParameters() as $id=>$parameter)
		{
			if(is_array($parameter))
			{
				$component=Prado::createComponent($parameter[0]);
				foreach($parameter[1] as $name=>$value)
					$component->setSubProperty($name,$value);
				$this->_parameters->add($id,$component);
			}
			else
				$this->_parameters->add($id,$parameter);
		}

		// load and init modules specified in app config
		$modules=array();
		foreach($config->getModules() as $id=>$moduleConfig)
		{
			Prado::trace("Loading module $id ({$moduleConfig[0]})",'System.TApplication');
			list($moduleClass, $initProperties, $configElement)=$moduleConfig;
			$module=Prado::createComponent($moduleClass);
			if(!is_string($id))
			{
				$id='_module'.count($this->_modules);
				$initProperties['id']=$id;
			}
			$this->setModule($id,$module);
			foreach($initProperties as $name=>$value)
				$module->setSubProperty($name,$value);
			$modules[]=array($module,$configElement);
		}
		foreach($modules as $module)
			$module[0]->init($module[1]);

		// load service
		foreach($config->getServices() as $serviceID=>$serviceConfig)
			$this->_services[$serviceID]=$serviceConfig;

		// external configurations
		foreach($config->getExternalConfigurations() as $filePath=>$condition)
		{
			if($condition!==true)
				$condition=$this->evaluateExpression($condition);
			if($condition)
			{
				if(($path=Prado::getPathOfNamespace($filePath,self::CONFIG_FILE_EXT))===null || !is_file($path))
					throw new TConfigurationException('application_includefile_invalid',$filePath);
				$c=new TApplicationConfiguration;
				$c->loadFromFile($path);
				$this->applyConfiguration($c,$withinService);
			}
		}
	}

	/**
	 * Loads configuration and initializes application.
	 * Configuration file will be read and parsed (if a valid cached version exists,
	 * it will be used instead). Then, modules are created and initialized;
	 * Afterwards, the requested service is created and initialized.
	 * @param string configuration file path (absolute or relative to current executing script)
	 * @param string cache file path, empty if no present or needed
	 * @throws TConfigurationException if module is redefined of invalid type, or service not defined or of invalid type
	 */
	protected function initApplication()
	{
		Prado::trace('Initializing application','System.TApplication');

		if($this->_configFile!==null)
		{
			if($this->_cacheFile===null || @filemtime($this->_cacheFile)<filemtime($this->_configFile))
			{
				$config=new TApplicationConfiguration;
				$config->loadFromFile($this->_configFile);
				if($this->_cacheFile!==null)
					file_put_contents($this->_cacheFile,Prado::serialize($config),LOCK_EX);
			}
			else
				$config=Prado::unserialize(file_get_contents($this->_cacheFile));

			$this->applyConfiguration($config,false);
		}

		if(($serviceID=$this->getRequest()->resolveRequest(array_keys($this->_services)))===null)
			$serviceID=self::PAGE_SERVICE_ID;

		$this->startService($serviceID);
	}

	/**
	 * Starts the specified service.
	 * The service instance will be created. Its properties will be initialized
	 * and the configurations will be applied, if any.
	 * @param string service ID
	 */
	public function startService($serviceID)
	{
		if(isset($this->_services[$serviceID]))
		{
			list($serviceClass,$initProperties,$configElement)=$this->_services[$serviceID];
			$service=Prado::createComponent($serviceClass);
			if(!($service instanceof IService))
				throw new THttpException(500,'application_service_invalid',$serviceClass);
			if(!$service->getEnabled())
				throw new THttpException(500,'application_service_unavailable',$serviceClass);
			$service->setID($serviceID);
			$this->setService($service);

			foreach($initProperties as $name=>$value)
				$service->setSubProperty($name,$value);

			if($configElement!==null)
			{
				$config=new TApplicationConfiguration;
				$config->loadFromXml($configElement,$this->getBasePath());
				$this->applyConfiguration($config,true);
			}

			$service->init($configElement);
		}
		else
			throw new THttpException(500,'application_service_unknown',$serviceID);
	}

	/**
	 * Raises OnError event.
	 * This method is invoked when an exception is raised during the lifecycles
	 * of the application.
	 * @param mixed event parameter
	 */
	public function onError($param)
	{
		Prado::log($param->getMessage(),TLogger::ERROR,'System.TApplication');
		$this->raiseEvent('OnError',$this,$param);
		$this->getErrorHandler()->handleError($this,$param);
	}

	/**
	 * Raises OnBeginRequest event.
	 * At the time when this method is invoked, application modules are loaded
	 * and initialized, user request is resolved and the corresponding service
	 * is loaded and initialized. The application is about to start processing
	 * the user request.
	 */
	public function onBeginRequest()
	{
		$this->raiseEvent('OnBeginRequest',$this,null);
	}

	/**
	 * Raises OnAuthentication event.
	 * This method is invoked when the user request needs to be authenticated.
	 */
	public function onAuthentication()
	{
		$this->raiseEvent('OnAuthentication',$this,null);
	}

	/**
	 * Raises OnAuthenticationComplete event.
	 * This method is invoked right after the user request is authenticated.
	 */
	public function onAuthenticationComplete()
	{
		$this->raiseEvent('OnAuthenticationComplete',$this,null);
	}

	/**
	 * Raises OnAuthorization event.
	 * This method is invoked when the user request needs to be authorized.
	 */
	public function onAuthorization()
	{
		$this->raiseEvent('OnAuthorization',$this,null);
	}

	/**
	 * Raises OnAuthorizationComplete event.
	 * This method is invoked right after the user request is authorized.
	 */
	public function onAuthorizationComplete()
	{
		$this->raiseEvent('OnAuthorizationComplete',$this,null);
	}

	/**
	 * Raises OnLoadState event.
	 * This method is invoked when the application needs to load state (probably stored in session).
	 */
	public function onLoadState()
	{
		$this->loadGlobals();
		$this->raiseEvent('OnLoadState',$this,null);
	}

	/**
	 * Raises OnLoadStateComplete event.
	 * This method is invoked right after the application state has been loaded.
	 */
	public function onLoadStateComplete()
	{
		$this->raiseEvent('OnLoadStateComplete',$this,null);
	}

	/**
	 * Raises OnPreRunService event.
	 * This method is invoked right before the service is to be run.
	 */
	public function onPreRunService()
	{
		$this->raiseEvent('OnPreRunService',$this,null);
	}

	/**
	 * Runs the requested service.
	 */
	public function runService()
	{
		if($this->_service)
			$this->_service->run();
	}

	/**
	 * Raises OnSaveState event.
	 * This method is invoked when the application needs to save state (probably stored in session).
	 */
	public function onSaveState()
	{
		$this->raiseEvent('OnSaveState',$this,null);
		$this->saveGlobals();
	}

	/**
	 * Raises OnSaveStateComplete event.
	 * This method is invoked right after the application state has been saved.
	 */
	public function onSaveStateComplete()
	{
		$this->raiseEvent('OnSaveStateComplete',$this,null);
	}

	/**
	 * Raises OnPreFlushOutput event.
	 * This method is invoked right before the application flushes output to client.
	 */
	public function onPreFlushOutput()
	{
		$this->raiseEvent('OnPreFlushOutput',$this,null);
	}

	/**
	 * Flushes output to client side.
	 */
	public function flushOutput()
	{
		$this->getResponse()->flush();
	}

	/**
	 * Raises OnEndRequest event.
	 * This method is invoked when the application completes the processing of the request.
	 */
	public function onEndRequest()
	{
		$this->saveGlobals();  // save global state
		$this->raiseEvent('OnEndRequest',$this,null);
	}
}

/**
 * TApplicationMode class.
 * TApplicationMode defines the possible mode that an application can be set at by
 * setting {@link TApplication::setMode Mode}.
 * In particular, the following modes are defined
 * - Off: the application is not running. Any request to the application will obtain an error.
 * - Debug: the application is running in debug mode.
 * - Normal: the application is running in normal production mode.
 * - Performance: the application is running in performance mode.
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0.4
 */
class TApplicationMode extends TEnumerable
{
	const Off='Off';
	const Debug='Debug';
	const Normal='Normal';
	const Performance='Performance';
}


/**
 * TApplicationConfiguration class.
 *
 * This class is used internally by TApplication to parse and represent application configuration.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TApplicationConfiguration extends TComponent
{
	/**
	 * @var array list of application initial property values, indexed by property names
	 */
	private $_properties=array();
	/**
	 * @var array list of namespaces to be used
	 */
	private $_usings=array();
	/**
	 * @var array list of path aliases, indexed by alias names
	 */
	private $_aliases=array();
	/**
	 * @var array list of module configurations
	 */
	private $_modules=array();
	/**
	 * @var array list of service configurations
	 */
	private $_services=array();
	/**
	 * @var array list of parameters
	 */
	private $_parameters=array();
	/**
	 * @var array list of included configurations
	 */
	private $_includes=array();
	/**
	 * @var boolean whether this configuration contains actual stuff
	 */
	private $_empty=true;

	/**
	 * Parses the application configuration file.
	 * @param string configuration file name
	 * @throws TConfigurationException if there is any parsing error
	 */
	public function loadFromFile($fname)
	{
		$dom=new TXmlDocument;
		$dom->loadFromFile($fname);
		$this->loadFromXml($dom,dirname($fname));
	}

	/**
	 * @return boolean whether this configuration contains actual stuff
	 */
	public function getIsEmpty()
	{
		return $this->_empty;
	}

	/**
	 * Parses the application configuration given in terms of a TXmlElement.
	 * @param TXmlElement the XML element
	 * @param string the context path (for specifying relative paths)
	 */
	public function loadFromXml($dom,$configPath)
	{
		// application properties
		foreach($dom->getAttributes() as $name=>$value)
		{
			$this->_properties[$name]=$value;
			$this->_empty=false;
		}

		foreach($dom->getElements() as $element)
		{
			switch($element->getTagName())
			{
				case 'paths':
					$this->loadPathsXml($element,$configPath);
					break;
				case 'modules':
					$this->loadModulesXml($element,$configPath);
					break;
				case 'services':
					$this->loadServicesXml($element,$configPath);
					break;
				case 'parameters':
					$this->loadParametersXml($element,$configPath);
					break;
				case 'include':
					$this->loadExternalXml($element,$configPath);
					break;
				default:
					//throw new TConfigurationException('appconfig_tag_invalid',$element->getTagName());
					break;
			}
		}
	}

	/**
	 * Loads the paths XML node.
	 * @param TXmlElement the paths XML node
	 * @param string the context path (for specifying relative paths)
	 */
	protected function loadPathsXml($pathsNode,$configPath)
	{
		foreach($pathsNode->getElements() as $element)
		{
			switch($element->getTagName())
			{
				case 'alias':
				{
					if(($id=$element->getAttribute('id'))!==null && ($path=$element->getAttribute('path'))!==null)
					{
						$path=str_replace('\\','/',$path);
						if(preg_match('/^\\/|.:\\/|.:\\\\/',$path))	// if absolute path
							$p=realpath($path);
						else
							$p=realpath($configPath.DIRECTORY_SEPARATOR.$path);
						if($p===false || !is_dir($p))
							throw new TConfigurationException('appconfig_aliaspath_invalid',$id,$path);
						if(isset($this->_aliases[$id]))
							throw new TConfigurationException('appconfig_alias_redefined',$id);
						$this->_aliases[$id]=$p;
					}
					else
						throw new TConfigurationException('appconfig_alias_invalid');
					$this->_empty=false;
					break;
				}
				case 'using':
				{
					if(($namespace=$element->getAttribute('namespace'))!==null)
						$this->_usings[]=$namespace;
					else
						throw new TConfigurationException('appconfig_using_invalid');
					$this->_empty=false;
					break;
				}
				default:
					throw new TConfigurationException('appconfig_paths_invalid',$tagName);
			}
		}
	}

	/**
	 * Loads the modules XML node.
	 * @param TXmlElement the modules XML node
	 * @param string the context path (for specifying relative paths)
	 */
	protected function loadModulesXml($modulesNode,$configPath)
	{
		foreach($modulesNode->getElements() as $element)
		{
			if($element->getTagName()==='module')
			{
				$properties=$element->getAttributes();
				$id=$properties->itemAt('id');
				$type=$properties->remove('class');
				if($type===null)
					throw new TConfigurationException('appconfig_moduletype_required',$id);
				$element->setParent(null);
				if($id===null)
					$this->_modules[]=array($type,$properties->toArray(),$element);
				else
					$this->_modules[$id]=array($type,$properties->toArray(),$element);
				$this->_empty=false;
			}
			else
				throw new TConfigurationException('appconfig_modules_invalid',$element->getTagName());
		}
	}

	/**
	 * Loads the services XML node.
	 * @param TXmlElement the services XML node
	 * @param string the context path (for specifying relative paths)
	 */
	protected function loadServicesXml($servicesNode,$configPath)
	{
		foreach($servicesNode->getElements() as $element)
		{
			if($element->getTagName()==='service')
			{
				$properties=$element->getAttributes();
				if(($id=$properties->itemAt('id'))===null)
					throw new TConfigurationException('appconfig_serviceid_required');
				if(($type=$properties->remove('class'))===null)
					throw new TConfigurationException('appconfig_servicetype_required',$id);
				$element->setParent(null);
				$this->_services[$id]=array($type,$properties->toArray(),$element);
				$this->_empty=false;
			}
			else
				throw new TConfigurationException('appconfig_services_invalid',$element->getTagName());
		}
	}

	/**
	 * Loads the parameters XML node.
	 * @param TXmlElement the parameters XML node
	 * @param string the context path (for specifying relative paths)
	 */
	protected function loadParametersXml($parametersNode,$configPath)
	{
		foreach($parametersNode->getElements() as $element)
		{
			if($element->getTagName()==='parameter')
			{
				$properties=$element->getAttributes();
				if(($id=$properties->remove('id'))===null)
					throw new TConfigurationException('appconfig_parameterid_required');
				if(($type=$properties->remove('class'))===null)
				{
					if(($value=$properties->remove('value'))===null)
						$this->_parameters[$id]=$element;
					else
						$this->_parameters[$id]=$value;
				}
				else
					$this->_parameters[$id]=array($type,$properties->toArray());
				$this->_empty=false;
			}
			else
				throw new TConfigurationException('appconfig_parameters_invalid',$element->getTagName());
		}
	}

	/**
	 * Loads the external XML configurations.
	 * @param TXmlElement the application DOM element
	 * @param string the context path (for specifying relative paths)
	 */
	protected function loadExternalXml($includeNode,$configPath)
	{
		if(($when=$includeNode->getAttribute('when'))===null)
			$when=true;
		if(($filePath=$includeNode->getAttribute('file'))===null)
			throw new TConfigurationException('appconfig_includefile_required');
		if(isset($this->_includes[$filePath]))
			$this->_includes[$filePath]='('.$this->_includes[$filePath].') || ('.$when.')';
		else
			$this->_includes[$filePath]=$when;
		$this->_empty=false;
	}

	/**
	 * Returns list of page initial property values.
	 * Each array element represents a single property with the key
	 * being the property name and the value the initial property value.
	 * @return array list of page initial property values
	 */
	public function getProperties()
	{
		return $this->_properties;
	}

	/**
	 * Returns list of path alias definitions.
	 * The definitions are aggregated (top-down) from configuration files along the path
	 * to the specified page. Each array element represents a single alias definition,
	 * with the key being the alias name and the value the absolute path.
	 * @return array list of path alias definitions
	 */
	public function getAliases()
	{
		return $this->_aliases;
	}

	/**
	 * Returns list of namespaces to be used.
	 * The namespaces are aggregated (top-down) from configuration files along the path
	 * to the specified page. Each array element represents a single namespace usage,
	 * with the value being the namespace to be used.
	 * @return array list of namespaces to be used
	 */
	public function getUsings()
	{
		return $this->_usings;
	}

	/**
	 * Returns list of module configurations.
	 * The module configurations are aggregated (top-down) from configuration files
	 * along the path to the specified page. Each array element represents
	 * a single module configuration, with the key being the module ID and
	 * the value the module configuration. Each module configuration is
	 * stored in terms of an array with the following content
	 * ([0]=>module type, [1]=>module properties, [2]=>complete module configuration)
	 * The module properties are an array of property values indexed by property names.
	 * The complete module configuration is a TXmlElement object representing
	 * the raw module configuration which may contain contents enclosed within
	 * module tags.
	 * @return array list of module configurations to be used
	 */
	public function getModules()
	{
		return $this->_modules;
	}

	/**
	 * @return array list of service configurations
	 */
	public function getServices()
	{
		return $this->_services;
	}

	/**
	 * Returns list of parameter definitions.
	 * The parameter definitions are aggregated (top-down) from configuration files
	 * along the path to the specified page. Each array element represents
	 * a single parameter definition, with the key being the parameter ID and
	 * the value the parameter definition. A parameter definition can be either
	 * a string representing a string-typed parameter, or an array.
	 * The latter defines a component-typed parameter whose format is as follows,
	 * ([0]=>component type, [1]=>component properties)
	 * The component properties are an array of property values indexed by property names.
	 * @return array list of parameter definitions to be used
	 */
	public function getParameters()
	{
		return $this->_parameters;
	}

	/**
	 * @return array list of external configuration files. Each element is like $filePath=>$condition
	 */
	public function getExternalConfigurations()
	{
		return $this->_includes;
	}
}

/**
 * TApplicationStatePersister class.
 * TApplicationStatePersister provides a file-based persistent storage
 * for application state. Application state, when serialized, is stored
 * in a file named 'global.cache' under the 'runtime' directory of the application.
 * Cache will be exploited if it is enabled.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System
 * @since 3.0
 */
class TApplicationStatePersister extends TModule implements IStatePersister
{
	/**
	 * Name of the value stored in cache
	 */
	const CACHE_NAME='prado:appstate';

	/**
	 * Initializes module.
	 * @param TXmlElement module configuration (may be null)
	 */
	public function init($config)
	{
		$this->getApplication()->setApplicationStatePersister($this);
	}

	/**
	 * @return string the file path storing the application state
	 */
	protected function getStateFilePath()
	{
		return $this->getApplication()->getRuntimePath().'/global.cache';
	}

	/**
	 * Loads application state from persistent storage.
	 * @return mixed application state
	 */
	public function load()
	{
		if(($cache=$this->getApplication()->getCache())!==null && ($value=$cache->get(self::CACHE_NAME))!==false)
			return unserialize($value);
		else
		{
			if(($content=@file_get_contents($this->getStateFilePath()))!==false)
				return unserialize($content);
			else
				return null;
		}
	}

	/**
	 * Saves application state in persistent storage.
	 * @param mixed application state
	 */
	public function save($state)
	{
		$content=serialize($state);
		$saveFile=true;
		if(($cache=$this->getApplication()->getCache())!==null)
		{
			if($cache->get(self::CACHE_NAME)===$content)
				$saveFile=false;
			else
				$cache->set(self::CACHE_NAME,$content);
		}
		if($saveFile)
		{
			$fileName=$this->getStateFilePath();
			file_put_contents($fileName,$content,LOCK_EX);
		}
	}

}
?>