<?php
/**
 * TSqlMapXmlConfigBuilder, TSqlMapXmlConfiguration, TSqlMapXmlMappingConfiguration classes file.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2008 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Data.SqlMap.Configuration
 */

Prado::using('System.Data.SqlMap.Configuration.TSqlMapStatement');

/**
 * TSqlMapXmlConfig class file.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.SqlMap.Configuration
 */
abstract class TSqlMapXmlConfigBuilder
{
	/**
	 * Create an instance of an object give by the attribute named 'class' in the
	 * node and set the properties on the object given by attribute names and values.
	 * @param SimpleXmlNode property node
	 * @return Object new instance of class with class name given by 'class' attribute value.
	 */
	protected function createObjectFromNode($node)
	{
		if(isset($node['class']))
		{
			$obj = Prado::createComponent((string)$node['class']);
			$this->setObjectPropFromNode($obj,$node,array('class'));
			return $obj;
		}
		throw new TSqlMapConfigurationException(
			'sqlmap_node_class_undef', $node, $this->getConfigFile());
	}

	/**
	 * For each attributes (excluding attribute named in $except) set the
	 * property of the $obj given by the name of the attribute with the value
	 * of the attribute.
	 * @param Object object instance
	 * @param SimpleXmlNode property node
	 * @param array exception property name
	 */
	protected function setObjectPropFromNode($obj,$node,$except=array())
	{
		foreach($node->attributes() as $name=>$value)
		{
			if(!in_array($name,$except))
			{
				if($obj->canSetProperty($name))
					$obj->{$name} = (string)$value;
				else
					throw new TSqlMapConfigurationException(
						'sqlmap_invalid_property', $name, get_class($obj),
						$node, $this->getConfigFile());
			}
		}
	}

	/**
	 * Gets the filename relative to the basefile.
	 * @param string base filename
	 * @param string relative filename
	 * @return string absolute filename.
	 */
	protected function getAbsoluteFilePath($basefile,$resource)
	{
		$basedir = dirname($basefile);
		$file = realpath($basedir.DIRECTORY_SEPARATOR.$resource);
		if(!is_string($file) || !is_file($file))
			$file = realpath($resource);
		if(is_string($file) && is_file($file))
			return $file;
		else
			throw new TSqlMapConfigurationException(
				'sqlmap_unable_to_find_resource', $resource);
	}

	/**
	 * Load document using simple xml.
	 * @param string filename.
	 * @return SimpleXmlElement xml document.
	 */
	protected function loadXmlDocument($filename,TSqlMapXmlConfiguration $config)
	{
		if( strpos($filename, '${') !== false)
			$filename = $config->replaceProperties($filename);

		if(!is_file($filename))
			throw new TSqlMapConfigurationException(
				'sqlmap_unable_to_find_config', $filename);
		return simplexml_load_string($config->replaceProperties(file_get_contents($filename)));
	}

	/**
	 * Get element node by ID value (try for attribute name ID as case insensitive).
	 * @param SimpleXmlDocument $document
	 * @param string tag name.
	 * @param string id value.
	 * @return SimpleXmlElement node if found, null otherwise.
	 */
	protected function getElementByIdValue($document, $tag, $value)
	{
		//hack to allow upper case and lower case attribute names.
		foreach(array('id','ID','Id', 'iD') as $id)
		{
			$xpath = "//{$tag}[@{$id}='{$value}']";
			foreach($document->xpath($xpath) as $node)
				return $node;
		}
	}

	/**
	 * @return string configuration file.
	 */
	protected abstract function getConfigFile();
}

/**
 * TSqlMapXmlConfig class.
 *
 * Configures the TSqlMapManager using xml configuration file.
 *
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.SqlMap.Configuration
 * @since 3.1
 */
class TSqlMapXmlConfiguration extends TSqlMapXmlConfigBuilder
{
	/**
	 * @var TSqlMapManager manager
	 */
	private $_manager;
	/**
	 * @var string configuration file.
	 */
	private $_configFile;
	/**
	 * @var array global properties.
	 */
	private $_properties=array();

	/**
	 * @param TSqlMapManager manager instance.
	 */
	public function __construct($manager)
	{
		$this->_manager=$manager;
	}

	public function getManager()
	{
		return $this->_manager;
	}

	protected function getConfigFile()
	{
		return $this->_configFile;
	}

	/**
	 * Configure the TSqlMapManager using the given xml file.
	 * @param string SqlMap configuration xml file.
	 */
	public function configure($filename=null)
	{
		$this->_configFile=$filename;
		$document = $this->loadXmlDocument($filename,$this);

		foreach($document->xpath('//property') as $property)
			$this->loadGlobalProperty($property);

		foreach($document->xpath('//typeHandler') as $handler)
			$this->loadTypeHandler($handler);

		foreach($document->xpath('//connection[last()]') as $conn)
			$this->loadDatabaseConnection($conn);

		//try to load configuration in the current config file.
		$mapping = new TSqlMapXmlMappingConfiguration($this);
		$mapping->configure($filename);

		foreach($document->xpath('//sqlMap') as $sqlmap)
			$this->loadSqlMappingFiles($sqlmap);

		$this->resolveResultMapping();
		$this->attachCacheModels();
	}

	/**
	 * Load global replacement property.
	 * @param SimpleXmlElement property node.
	 */
	protected function loadGlobalProperty($node)
	{
		$this->_properties[(string)$node['name']] = (string)$node['value'];
	}

	/**
	 * Load the type handler configurations.
	 * @param SimpleXmlElement type handler node
	 */
	protected function loadTypeHandler($node)
	{
		$handler = $this->createObjectFromNode($node);
		$this->_manager->getTypeHandlers()->registerTypeHandler($handler);
	}

	/**
	 * Load the database connection tag.
	 * @param SimpleXmlElement connection node.
	 */
	protected function loadDatabaseConnection($node)
	{
		$conn = $this->createObjectFromNode($node);
		$this->_manager->setDbConnection($conn);
	}

	/**
	 * Load SqlMap mapping configuration.
	 * @param unknown_type $node
	 */
	protected function loadSqlMappingFiles($node)
	{
		if(strlen($resource = (string)$node['resource']) > 0)
		{
			if( strpos($resource, '${') !== false)
				$resource = $this->replaceProperties($resource);

			$mapping = new TSqlMapXmlMappingConfiguration($this);
			$filename = $this->getAbsoluteFilePath($this->_configFile, $resource);
			$mapping->configure($filename);
		}
	}

	/**
	 * Resolve nest result mappings.
	 */
	protected function resolveResultMapping()
	{
		$maps = $this->_manager->getResultMaps();
		foreach($maps as $entry)
		{
			foreach($entry->getColumns() as $item)
			{
				$resultMap = $item->getResultMapping();
				if(strlen($resultMap) > 0)
				{
					if($maps->contains($resultMap))
						$item->setNestedResultMap($maps[$resultMap]);
					else
						throw new TSqlMapConfigurationException(
							'sqlmap_unable_to_find_result_mapping',
								$resultMap, $this->_configFile, $entry->getID());
				}
			}
			if($entry->getDiscriminator()!==null)
				$entry->getDiscriminator()->initialize($this->_manager);
		}
	}

	/**
	 * Set the cache for each statement having a cache model property.
	 */
	protected function attachCacheModels()
	{
		foreach($this->_manager->getMappedStatements() as $mappedStatement)
		{
			if(strlen($model = $mappedStatement->getStatement()->getCacheModel()) > 0)
			{
				$cache = $this->_manager->getCacheModel($model);
				$mappedStatement->getStatement()->setCache($cache);
			}
		}
	}

	/**
	 * Replace the place holders ${name} in text with properties the
	 * corresponding global property value.
	 * @param string original string.
	 * @return string string with global property replacement.
	 */
	public function replaceProperties($string)
	{
		foreach($this->_properties as $find => $replace)
			$string = str_replace('${'.$find.'}', $replace, $string);
		return $string;
	}
}

/**
 * Loads the statements, result maps, parameters maps from xml configuration.
 *
 * description
 *
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.SqlMap.Configuration
 * @since 3.1
 */
class TSqlMapXmlMappingConfiguration extends TSqlMapXmlConfigBuilder
{
	private $_xmlConfig;
	private $_configFile;
	private $_manager;

	private $_document;

	private $_FlushOnExecuteStatements=array();

	/**
	 * Regular expressions for escaping simple/inline parameter symbols
	 */
	const SIMPLE_MARK='$';
	const INLINE_SYMBOL='#';
	const ESCAPED_SIMPLE_MARK_REGEXP='/\$\$/';
	const ESCAPED_INLINE_SYMBOL_REGEXP='/\#\#/';
	const SIMPLE_PLACEHOLDER='`!!`';
	const INLINE_PLACEHOLDER='`!!!`';

	/**
	 * @param TSqlMapXmlConfiguration parent xml configuration.
	 */
	public function __construct(TSqlMapXmlConfiguration $xmlConfig)
	{
		$this->_xmlConfig=$xmlConfig;
		$this->_manager=$xmlConfig->getManager();
	}

	protected function getConfigFile()
	{
		return $this->_configFile;
	}

	/**
	 * Configure an XML mapping.
	 * @param string xml mapping filename.
	 */
	public function configure($filename)
	{
		$this->_configFile=$filename;
		$document = $this->loadXmlDocument($filename,$this->_xmlConfig);
		$this->_document=$document;

		static $bCacheDependencies;
		if($bCacheDependencies === null)
			$bCacheDependencies = Prado::getApplication()->getMode() !== TApplicationMode::Performance;

		if($bCacheDependencies)
			$this->_manager->getCacheDependencies()
					->getDependencies()
					->add(new TFileCacheDependency($filename));

		foreach($document->xpath('//resultMap') as $node)
			$this->loadResultMap($node);

		foreach($document->xpath('//parameterMap') as $node)
			$this->loadParameterMap($node);

		foreach($document->xpath('//statement') as $node)
			$this->loadStatementTag($node);

		foreach($document->xpath('//select') as $node)
			$this->loadSelectTag($node);

		foreach($document->xpath('//insert') as $node)
			$this->loadInsertTag($node);

		foreach($document->xpath('//update') as $node)
			$this->loadUpdateTag($node);

		foreach($document->xpath('//delete') as $node)
			$this->loadDeleteTag($node);

		foreach($document->xpath('//procedure') as $node)
			$this->loadProcedureTag($node);

		foreach($document->xpath('//cacheModel') as $node)
				$this->loadCacheModel($node);

		$this->registerCacheTriggers();
	}

	/**
	 * Load the result maps.
	 * @param SimpleXmlElement result map node.
	 */
	protected function loadResultMap($node)
	{
		$resultMap = $this->createResultMap($node);

		//find extended result map.
		if(strlen($extendMap = $resultMap->getExtends()) > 0)
		{
			if(!$this->_manager->getResultMaps()->contains($extendMap))
			{
				$extendNode=$this->getElementByIdValue($this->_document,'resultMap',$extendMap);
				if($extendNode!==null)
					$this->loadResultMap($extendNode);
			}

			if(!$this->_manager->getResultMaps()->contains($extendMap))
				throw new TSqlMapConfigurationException(
					'sqlmap_unable_to_find_parent_result_map', $node, $this->_configFile, $extendMap);

			$superMap = $this->_manager->getResultMap($extendMap);
			$resultMap->getColumns()->mergeWith($superMap->getColumns());
		}

		//add the result map
		if(!$this->_manager->getResultMaps()->contains($resultMap->getID()))
			$this->_manager->addResultMap($resultMap);
	}

	/**
	 * Create a new result map and its associated result properties,
	 * disciminiator and sub maps.
	 * @param SimpleXmlElement result map node
	 * @return TResultMap SqlMap result mapping.
	 */
	protected function createResultMap($node)
	{
		$resultMap = new TResultMap();
		$this->setObjectPropFromNode($resultMap,$node);

		//result nodes
		foreach($node->result as $result)
		{
			$property = new TResultProperty($resultMap);
			$this->setObjectPropFromNode($property,$result);
			$resultMap->addResultProperty($property);
		}

		//create the discriminator
		$discriminator = null;
		if(isset($node->discriminator))
		{
			$discriminator = new TDiscriminator();
			$this->setObjectPropFromNode($discriminator, $node->discriminator);
			$discriminator->initMapping($resultMap);
		}

		foreach($node->xpath('subMap') as $subMapNode)
		{
			if($discriminator===null)
				throw new TSqlMapConfigurationException(
					'sqlmap_undefined_discriminator', $node, $this->_configFile,$subMapNode);
			$subMap = new TSubMap;
			$this->setObjectPropFromNode($subMap,$subMapNode);
			$discriminator->addSubMap($subMap);
		}

		if($discriminator!==null)
			$resultMap->setDiscriminator($discriminator);

		return $resultMap;
	}

	/**
	 * Load parameter map from xml.
	 *
	 * @param SimpleXmlElement parameter map node.
	 */
	protected function loadParameterMap($node)
	{
		$parameterMap = $this->createParameterMap($node);

		if(strlen($extendMap = $parameterMap->getExtends()) > 0)
		{
			if(!$this->_manager->getParameterMaps()->contains($extendMap))
			{
				$extendNode=$this->getElementByIdValue($this->_document,'parameterMap',$extendMap);
				if($extendNode!==null)
					$this->loadParameterMap($extendNode);
			}

			if(!$this->_manager->getParameterMaps()->contains($extendMap))
				throw new TSqlMapConfigurationException(
					'sqlmap_unable_to_find_parent_parameter_map', $node, $this->_configFile,$extendMap);
			$superMap = $this->_manager->getParameterMap($extendMap);
			$index = 0;
			foreach($superMap->getPropertyNames() as $propertyName)
				$parameterMap->insertProperty($index++,$superMap->getProperty($propertyName));
		}
		$this->_manager->addParameterMap($parameterMap);
	}

	/**
	 * Create a new parameter map from xml node.
	 * @param SimpleXmlElement parameter map node.
	 * @return TParameterMap new parameter mapping.
	 */
	protected function createParameterMap($node)
	{
		$parameterMap = new TParameterMap();
		$this->setObjectPropFromNode($parameterMap,$node);
		foreach($node->parameter as $parameter)
		{
			$property = new TParameterProperty();
			$this->setObjectPropFromNode($property,$parameter);
			$parameterMap->addProperty($property);
		}
		return $parameterMap;
	}

	/**
	 * Load statement mapping from xml configuration file.
	 * @param SimpleXmlElement statement node.
	 */
	protected function loadStatementTag($node)
	{
		$statement = new TSqlMapStatement();
		$this->setObjectPropFromNode($statement,$node);
		$this->processSqlStatement($statement, $node);
		$mappedStatement = new TMappedStatement($this->_manager, $statement);
		$this->_manager->addMappedStatement($mappedStatement);
	}

	/**
	 * Load extended SQL statements if application. Replaces global properties
	 * in the sql text. Extracts inline parameter maps.
	 * @param TSqlMapStatement mapped statement.
	 * @param SimpleXmlElement statement node.
	 */
	protected function processSqlStatement($statement, $node)
	{
		$commandText = (string)$node;
		if(strlen($extend = $statement->getExtends()) > 0)
		{
			$superNode = $this->getElementByIdValue($this->_document,'*',$extend);
			if($superNode!==null)
				$commandText = (string)$superNode . $commandText;
			else
				throw new TSqlMapConfigurationException(
						'sqlmap_unable_to_find_parent_sql', $extend, $this->_configFile,$node);
		}
		//$commandText = $this->_xmlConfig->replaceProperties($commandText);
		$statement->initialize($this->_manager);
		$this->applyInlineParameterMap($statement, $commandText, $node);
	}

	/**
	 * Extract inline parameter maps.
	 * @param TSqlMapStatement statement object.
	 * @param string sql text
	 * @param SimpleXmlElement statement node.
	 */
	protected function applyInlineParameterMap($statement, $sqlStatement, $node)
	{
		$scope['file'] = $this->_configFile;
		$scope['node'] = $node;

		$sqlStatement=preg_replace(self::ESCAPED_INLINE_SYMBOL_REGEXP,self::INLINE_PLACEHOLDER,$sqlStatement);
		if($statement->parameterMap() === null)
		{
			// Build a Parametermap with the inline parameters.
			// if they exist. Then delete inline infos from sqltext.
			$parameterParser = new TInlineParameterMapParser;
			$sqlText = $parameterParser->parse($sqlStatement, $scope);
			if(count($sqlText['parameters']) > 0)
			{
				$map = new TParameterMap();
				$map->setID($statement->getID().'-InLineParameterMap');
				$statement->setInlineParameterMap($map);
				foreach($sqlText['parameters'] as $property)
					$map->addProperty($property);
			}
			$sqlStatement = $sqlText['sql'];
		}
		$sqlStatement=preg_replace('/'.self::INLINE_PLACEHOLDER.'/',self::INLINE_SYMBOL,$sqlStatement);

		$this->prepareSql($statement, $sqlStatement, $node);
	}

	/**
	 * Prepare the sql text (may extend to dynamic sql).
	 * @param TSqlMapStatement mapped statement.
	 * @param string sql text.
	 * @param SimpleXmlElement statement node.
	 * @todo Extend to dynamic sql.
	 */
	protected function prepareSql($statement,$sqlStatement, $node)
	{
		$simpleDynamic = new TSimpleDynamicParser;
		$sqlStatement=preg_replace(self::ESCAPED_SIMPLE_MARK_REGEXP,self::SIMPLE_PLACEHOLDER,$sqlStatement);
		$dynamics = $simpleDynamic->parse($sqlStatement);
		if(count($dynamics['parameters']) > 0)
		{
			$sql = new TSimpleDynamicSql($dynamics['parameters']);
			$sqlStatement = $dynamics['sql'];
		}
		else
			$sql = new TStaticSql();
		$sqlStatement=preg_replace('/'.self::SIMPLE_PLACEHOLDER.'/',self::SIMPLE_MARK,$sqlStatement);
		$sql->buildPreparedStatement($statement, $sqlStatement);
		$statement->setSqlText($sql);
	}

	/**
	 * Load select statement from xml mapping.
	 * @param SimpleXmlElement select node.
	 */
	protected function loadSelectTag($node)
	{
		$select = new TSqlMapSelect;
		$this->setObjectPropFromNode($select,$node);
		$this->processSqlStatement($select,$node);
		$mappedStatement = new TMappedStatement($this->_manager, $select);
		if(strlen($select->getCacheModel()) > 0)
			$mappedStatement = new TCachingStatement($mappedStatement);

		$this->_manager->addMappedStatement($mappedStatement);
	}

	/**
	 * Load insert statement from xml mapping.
	 * @param SimpleXmlElement insert node.
	 */
	protected function loadInsertTag($node)
	{
		$insert = $this->createInsertStatement($node);
		$this->processSqlStatement($insert, $node);
		$mappedStatement = new TInsertMappedStatement($this->_manager, $insert);
		$this->_manager->addMappedStatement($mappedStatement);
	}

	/**
	 * Create new insert statement from xml node.
	 * @param SimpleXmlElement insert node.
	 * @return TSqlMapInsert insert statement.
	 */
	protected function createInsertStatement($node)
	{
		$insert = new TSqlMapInsert;
		$this->setObjectPropFromNode($insert,$node);
		if(isset($node->selectKey))
			$this->loadSelectKeyTag($insert,$node->selectKey);
		return $insert;
	}

	/**
	 * Load the selectKey statement from xml mapping.
	 * @param SimpleXmlElement selectkey node
	 */
	protected function loadSelectKeyTag($insert, $node)
	{
		$selectKey = new TSqlMapSelectKey;
		$this->setObjectPropFromNode($selectKey,$node);
		$selectKey->setID($insert->getID());
		$selectKey->setID($insert->getID().'.SelectKey');
		$this->processSqlStatement($selectKey,$node);
		$insert->setSelectKey($selectKey);
		$mappedStatement = new TMappedStatement($this->_manager, $selectKey);
		$this->_manager->addMappedStatement($mappedStatement);
	}

	/**
	 * Load update statement from xml mapping.
	 * @param SimpleXmlElement update node.
	 */
	protected function loadUpdateTag($node)
	{
		$update = new TSqlMapUpdate;
		$this->setObjectPropFromNode($update,$node);
		$this->processSqlStatement($update, $node);
		$mappedStatement = new TUpdateMappedStatement($this->_manager, $update);
		$this->_manager->addMappedStatement($mappedStatement);
	}

	/**
	 * Load delete statement from xml mapping.
	 * @param SimpleXmlElement delete node.
	 */
	protected function loadDeleteTag($node)
	{
		$delete = new TSqlMapDelete;
		$this->setObjectPropFromNode($delete,$node);
		$this->processSqlStatement($delete, $node);
		$mappedStatement = new TDeleteMappedStatement($this->_manager, $delete);
		$this->_manager->addMappedStatement($mappedStatement);
	}

	/**
	 * Load procedure statement from xml mapping.
	 * @todo Implement loading procedure
	 * @param SimpleXmlElement procedure node
	 */
	protected function loadProcedureTag($node)
	{
		//var_dump('todo: add load procedure');
	}

	/**
	 * Load cache models from xml mapping.
	 * @param SimpleXmlElement cache node.
	 */
	protected function loadCacheModel($node)
	{
		$cacheModel = new TSqlMapCacheModel;
		$properties = array('id','implementation');
		foreach($node->attributes() as $name=>$value)
		{
			if(in_array(strtolower($name), $properties))
				$cacheModel->{'set'.$name}((string)$value);
		}
		$cache = Prado::createComponent($cacheModel->getImplementationClass());
		$this->setObjectPropFromNode($cache,$node,$properties);

		foreach($node->xpath('property') as $propertyNode)
		{
			$name = $propertyNode->attributes()->name;
			if($name===null || $name==='') continue;

			$value = $propertyNode->attributes()->value;
			if($value===null || $value==='') continue;

			if( !TPropertyAccess::has($cache, $name) ) continue;

			TPropertyAccess::set($cache, $name, $value);
		}

		$this->loadFlushInterval($cacheModel,$node);

		$cacheModel->initialize($cache);
		$this->_manager->addCacheModel($cacheModel);
		foreach($node->xpath('flushOnExecute') as $flush)
			$this->loadFlushOnCache($cacheModel,$node,$flush);
	}

	/**
	 * Load the flush interval
	 * @param TSqlMapCacheModel cache model
	 * @param SimpleXmlElement cache node
	 */
	protected function loadFlushInterval($cacheModel, $node)
	{
		$flushInterval = $node->xpath('flushInterval');
		if($flushInterval === null || count($flushInterval) === 0) return;
		$duration = 0;
		foreach($flushInterval[0]->attributes() as $name=>$value)
		{
			switch(strToLower($name))
			{
				case 'seconds':
					$duration += (integer)$value;
				break;
				case 'minutes':
					$duration += 60 * (integer)$value;
				break;
				case 'hours':
					$duration += 3600 * (integer)$value;
				break;
				case 'days':
					$duration += 86400 * (integer)$value;
				break;
				case 'duration':
					$duration = (integer)$value;
				break 2; // switch, foreach
			}
		}
		$cacheModel->setFlushInterval($duration);
	}

	/**
	 * Load the flush on cache properties.
	 * @param TSqlMapCacheModel cache model
	 * @param SimpleXmlElement parent node.
	 * @param SimpleXmlElement flush node.
	 */
	protected function loadFlushOnCache($cacheModel,$parent,$node)
	{
		$id = $cacheModel->getID();
		if(!isset($this->_FlushOnExecuteStatements[$id]))
			$this->_FlushOnExecuteStatements[$id] = array();
		foreach($node->attributes() as $name=>$value)
		{
			if(strtolower($name)==='statement')
				$this->_FlushOnExecuteStatements[$id][] = (string)$value;
		}
	}

	/**
	 * Attach CacheModel to statement and register trigger statements for cache models
	 */
	protected function registerCacheTriggers()
	{
		foreach($this->_FlushOnExecuteStatements as $cacheID => $statementIDs)
		{
			$cacheModel = $this->_manager->getCacheModel($cacheID);
			foreach($statementIDs as $statementID)
			{
				$statement = $this->_manager->getMappedStatement($statementID);
				$cacheModel->registerTriggerStatement($statement);
			}
		}
	}
}