Subversion-Projekte lars-tiefland.prado

Revision

Blame | Letzte Änderung | Log anzeigen | RSS feed

<?php
/**
 * TTemplateManager and TTemplate class file
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2008 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id: TTemplateManager.php 2567 2008-11-12 16:53:52Z carlgmathisen $
 * @package System.Web.UI
 */

/**
 * Includes TOutputCache class file
 */
Prado::using('System.Web.UI.WebControls.TOutputCache');

/**
 * TTemplateManager class
 *
 * TTemplateManager manages the loading and parsing of control templates.
 *
 * There are two ways of loading a template, either by the associated template
 * control class name, or the template file name.
 * The former is via calling {@link getTemplateByClassName}, which tries to
 * locate the corresponding template file under the directory containing
 * the class file. The name of the template file is the class name with
 * the extension '.tpl'. To load a template from a template file path,
 * call {@link getTemplateByFileName}.
 *
 * By default, TTemplateManager is registered with {@link TPageService} as the
 * template manager module that can be accessed via {@link TPageService::getTemplateManager()}.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: TTemplateManager.php 2567 2008-11-12 16:53:52Z carlgmathisen $
 * @package System.Web.UI
 * @since 3.0
 */
class TTemplateManager extends TModule
{
        /**
         * Template file extension
         */
        const TEMPLATE_FILE_EXT='.tpl';
        /**
         * Prefix of the cache variable name for storing parsed templates
         */
        const TEMPLATE_CACHE_PREFIX='prado:template:';

        /**
         * Initializes the module.
         * This method is required by IModule and is invoked by application.
         * It starts output buffer if it is enabled.
         * @param TXmlElement module configuration
         */
        public function init($config)
        {
                $this->getService()->setTemplateManager($this);
        }

        /**
         * Loads the template corresponding to the specified class name.
         * @return ITemplate template for the class name, null if template doesn't exist.
         */
        public function getTemplateByClassName($className)
        {
                $class=new ReflectionClass($className);
                $tplFile=dirname($class->getFileName()).DIRECTORY_SEPARATOR.$className.self::TEMPLATE_FILE_EXT;
                return $this->getTemplateByFileName($tplFile);
        }

        /**
         * Loads the template from the specified file.
         * @return ITemplate template parsed from the specified file, null if the file doesn't exist.
         */
        public function getTemplateByFileName($fileName)
        {
                if(($fileName=$this->getLocalizedTemplate($fileName))!==null)
                {
                        Prado::trace("Loading template $fileName",'System.Web.UI.TTemplateManager');
                        if(($cache=$this->getApplication()->getCache())===null)
                                return new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName);
                        else
                        {
                                $array=$cache->get(self::TEMPLATE_CACHE_PREFIX.$fileName);
                                if(is_array($array))
                                {
                                        list($template,$timestamps)=$array;
                                        if($this->getApplication()->getMode()===TApplicationMode::Performance)
                                                return $template;
                                        $cacheValid=true;
                                        foreach($timestamps as $tplFile=>$timestamp)
                                        {
                                                if(!is_file($tplFile) || filemtime($tplFile)>$timestamp)
                                                {
                                                        $cacheValid=false;
                                                        break;
                                                }
                                        }
                                        if($cacheValid)
                                                return $template;
                                }
                                $template=new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName);
                                $includedFiles=$template->getIncludedFiles();
                                $timestamps=array();
                                $timestamps[$fileName]=filemtime($fileName);
                                foreach($includedFiles as $includedFile)
                                        $timestamps[$includedFile]=filemtime($includedFile);
                                $cache->set(self::TEMPLATE_CACHE_PREFIX.$fileName,array($template,$timestamps));
                                return $template;
                        }
                }
                else
                        return null;
        }

        /**
         * Finds a localized template file.
         * @param string template file.
         * @return string|null a localized template file if found, null otherwise.
         */
        protected function getLocalizedTemplate($filename)
        {
                if(($app=$this->getApplication()->getGlobalization(false))===null)
                        return is_file($filename)?$filename:null;
                foreach($app->getLocalizedResource($filename) as $file)
                {
                        if(($file=realpath($file))!==false && is_file($file))
                                return $file;
                }
                return null;
        }
}

/**
 * TTemplate implements PRADO template parsing logic.
 * A TTemplate object represents a parsed PRADO control template.
 * It can instantiate the template as child controls of a specified control.
 * The template format is like HTML, with the following special tags introduced,
 * - component tags: a component tag represents the configuration of a component.
 * The tag name is in the format of com:ComponentType, where ComponentType is the component
 * class name. Component tags must be well-formed. Attributes of the component tag
 * are treated as either property initial values, event handler attachment, or regular
 * tag attributes.
 * - property tags: property tags are used to set large block of attribute values.
 * The property tag name is in the format of <prop:AttributeName> where AttributeName
 * can be a property name, an event name or a regular tag attribute name.
 * - group subproperty tags: subproperties of a common property can be configured using
 * <prop:MainProperty SubProperty1="Value1" SubProperty2="Value2" .../>
 * - directive: directive specifies the property values for the template owner.
 * It is in the format of <%@ property name-value pairs %>;
 * - expressions: They are in the formate of <%= PHP expression %> and <%% PHP statements %>
 * - comments: There are two kinds of comments, regular HTML comments and special template comments.
 * The former is in the format of <!-- comments -->, which will be treated as text strings.
 * The latter is in the format of <!-- comments --!>, which will be stripped out.
 *
 * Tags other than the above are not required to be well-formed.
 *
 * A TTemplate object represents a parsed PRADO template. To instantiate the template
 * for a particular control, call {@link instantiateIn($control)}, which
 * will create and intialize all components specified in the template and
 * set their parent as $control.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: TTemplateManager.php 2567 2008-11-12 16:53:52Z carlgmathisen $
 * @package System.Web.UI
 * @since 3.0
 */
class TTemplate extends TApplicationComponent implements ITemplate
{
        /**
         *  '<!--.*?--!>' - template comments
     *  '<!--.*?-->'  - HTML comments
         *      '<\/?com:([\w\.]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/?>' - component tags
         *      '<\/?prop:([\w\.]+)\s*>'  - property tags
         *      '<%@\s*((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?")*)\s*%>'  - directives
         *      '<%[%#~\/\\$=\\[](.*?)%>'  - expressions
         *  '<prop:([\w\.]+)((?:\s*[\w\.]+=\'.*?\'|\s*[\w\.]+=".*?"|\s*[\w\.]+=<%.*?%>)*)\s*\/>' - group subproperty tags
         */
        const REGEX_RULES='/<!--.*?--!>|<!---.*?--->|<\/?com:([\w\.]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/?>|<\/?prop:([\w\.]+)\s*>|<%@\s*((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?")*)\s*%>|<%[%#~\/\\$=\\[](.*?)%>|<prop:([\w\.]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/>/msS';

        /**
         * Different configurations of component property/event/attribute
         */
        const CONFIG_DATABIND=0;
        const CONFIG_EXPRESSION=1;
        const CONFIG_ASSET=2;
        const CONFIG_PARAMETER=3;
        const CONFIG_LOCALIZATION=4;
        const CONFIG_TEMPLATE=5;

        /**
         * @var array list of component tags and strings
         */
        private $_tpl=array();
        /**
         * @var array list of directive settings
         */
        private $_directive=array();
        /**
         * @var string context path
         */
        private $_contextPath;
        /**
         * @var string template file path (if available)
         */
        private $_tplFile=null;
        /**
         * @var integer the line number that parsing starts from (internal use)
         */
        private $_startingLine=0;
        /**
         * @var string template content to be parsed
         */
        private $_content;
        /**
         * @var boolean whether this template is a source template
         */
        private $_sourceTemplate=true;
        /**
         * @var string hash code of the template
         */
        private $_hashCode='';
        private $_tplControl=null;
        private $_includedFiles=array();
        private $_includeAtLine=array();
        private $_includeLines=array();


        /**
         * Constructor.
         * The template will be parsed after construction.
         * @param string the template string
         * @param string the template context directory
         * @param string the template file, null if no file
         * @param integer the line number that parsing starts from (internal use)
         * @param boolean whether this template is a source template, i.e., this template is loaded from
         * some external storage rather than from within another template.
         */
        public function __construct($template,$contextPath,$tplFile=null,$startingLine=0,$sourceTemplate=true)
        {
                $this->_sourceTemplate=$sourceTemplate;
                $this->_contextPath=$contextPath;
                $this->_tplFile=$tplFile;
                $this->_startingLine=$startingLine;
                $this->_content=$template;
                $this->_hashCode=md5($template);
                $this->parse($template);
                $this->_content=null; // reset to save memory
        }

        /**
         * @return string  template file path if available, null otherwise.
         */
        public function getTemplateFile()
        {
                return $this->_tplFile;
        }

        /**
         * @return boolean whether this template is a source template, i.e., this template is loaded from
         * some external storage rather than from within another template.
         */
        public function getIsSourceTemplate()
        {
                return $this->_sourceTemplate;
        }

        /**
         * @return string context directory path
         */
        public function getContextPath()
        {
                return $this->_contextPath;
        }

        /**
         * @return array name-value pairs declared in the directive
         */
        public function getDirective()
        {
                return $this->_directive;
        }

        /**
         * @return string hash code that can be used to identify the template
         */
        public function getHashCode()
        {
                return $this->_hashCode;
        }

        /**
         * @return array the parsed template
         */
        public function &getItems()
        {
                return $this->_tpl;
        }

        /**
         * Instantiates the template.
         * Content in the template will be instantiated as components and text strings
         * and passed to the specified parent control.
         * @param TControl the control who owns the template
         * @param TControl the control who will become the root parent of the controls on the template. If null, it uses the template control.
         */
        public function instantiateIn($tplControl,$parentControl=null)
        {
                $this->_tplControl=$tplControl;
                if($parentControl===null)
                        $parentControl=$tplControl;
                if(($page=$tplControl->getPage())===null)
                        $page=$this->getService()->getRequestedPage();
                $controls=array();
                $directChildren=array();
                foreach($this->_tpl as $key=>$object)
                {
                        if($object[0]===-1)
                                $parent=$parentControl;
                        else if(isset($controls[$object[0]]))
                                $parent=$controls[$object[0]];
                        else
                                continue;
                        if(isset($object[2]))   // component
                        {
                                $component=Prado::createComponent($object[1]);
                                $properties=&$object[2];
                                if($component instanceof TControl)
                                {
                                        if($component instanceof TOutputCache)
                                                $component->setCacheKeyPrefix($this->_hashCode.$key);
                                        $component->setTemplateControl($tplControl);
                                        if(isset($properties['id']))
                                        {
                                                if(is_array($properties['id']))
                                                        $properties['id']=$component->evaluateExpression($properties['id'][1]);
                                                $tplControl->registerObject($properties['id'],$component);
                                        }
                                        if(isset($properties['skinid']))
                                        {
                                                if(is_array($properties['skinid']))
                                                        $component->setSkinID($component->evaluateExpression($properties['skinid'][1]));
                                                else
                                                        $component->setSkinID($properties['skinid']);
                                                unset($properties['skinid']);
                                        }

                                        $component->trackViewState(false);

                                        $component->applyStyleSheetSkin($page);
                                        foreach($properties as $name=>$value)
                                                $this->configureControl($component,$name,$value);

                                        $component->trackViewState(true);

                                        if($parent===$parentControl)
                                                $directChildren[]=$component;
                                        else
                                                $component->createdOnTemplate($parent);
                                        if($component->getAllowChildControls())
                                                $controls[$key]=$component;
                                }
                                else if($component instanceof TComponent)
                                {
                                        $controls[$key]=$component;
                                        if(isset($properties['id']))
                                        {
                                                if(is_array($properties['id']))
                                                        $properties['id']=$component->evaluateExpression($properties['id'][1]);
                                                $tplControl->registerObject($properties['id'],$component);
                                                if(!$component->hasProperty('id'))
                                                        unset($properties['id']);
                                        }
                                        foreach($properties as $name=>$value)
                                                $this->configureComponent($component,$name,$value);
                                        if($parent===$parentControl)
                                                $directChildren[]=$component;
                                        else
                                                $component->createdOnTemplate($parent);
                                }
                        }
                        else
                        {
                                if($object[1] instanceof TCompositeLiteral)
                                {
                                        // need to clone a new object because the one in template is reused
                                        $o=clone $object[1];
                                        $o->setContainer($tplControl);
                                        if($parent===$parentControl)
                                                $directChildren[]=$o;
                                        else
                                                $parent->addParsedObject($o);
                                }
                                else
                                {
                                        if($parent===$parentControl)
                                                $directChildren[]=$object[1];
                                        else
                                                $parent->addParsedObject($object[1]);
                                }
                        }
                }
                // delay setting parent till now because the parent may cause
                // the child to do lifecycle catchup which may cause problem
                // if the child needs its own child controls.
                foreach($directChildren as $control)
                {
                        if($control instanceof TComponent)
                                $control->createdOnTemplate($parentControl);
                        else
                                $parentControl->addParsedObject($control);
                }
        }

        /**
         * Configures a property/event of a control.
         * @param TControl control to be configured
         * @param string property name
         * @param mixed property initial value
         */
        protected function configureControl($control,$name,$value)
        {
                if(strncasecmp($name,'on',2)===0)               // is an event
                        $this->configureEvent($control,$name,$value,$control);
                else if(($pos=strrpos($name,'.'))===false)      // is a simple property or custom attribute
                        $this->configureProperty($control,$name,$value);
                else    // is a subproperty
                        $this->configureSubProperty($control,$name,$value);
        }

        /**
         * Configures a property of a non-control component.
         * @param TComponent component to be configured
         * @param string property name
         * @param mixed property initial value
         */
        protected function configureComponent($component,$name,$value)
        {
                if(strpos($name,'.')===false)   // is a simple property or custom attribute
                        $this->configureProperty($component,$name,$value);
                else    // is a subproperty
                        $this->configureSubProperty($component,$name,$value);
        }

        /**
         * Configures an event for a control.
         * @param TControl control to be configured
         * @param string event name
         * @param string event handler
         * @param TControl context control
         */
        protected function configureEvent($control,$name,$value,$contextControl)
        {
                if(strpos($value,'.')===false)
                        $control->attachEventHandler($name,array($contextControl,'TemplateControl.'.$value));
                else
                        $control->attachEventHandler($name,array($contextControl,$value));
        }

        /**
         * Configures a simple property for a component.
         * @param TComponent component to be configured
         * @param string property name
         * @param mixed property initial value
         */
        protected function configureProperty($component,$name,$value)
        {
                if(is_array($value))
                {
                        switch($value[0])
                        {
                                case self::CONFIG_DATABIND:
                                        $component->bindProperty($name,$value[1]);
                                        break;
                                case self::CONFIG_EXPRESSION:
                                        if($component instanceof TControl)
                                                $component->autoBindProperty($name,$value[1]);
                                        else
                                        {
                                                $setter='set'.$name;
                                                $component->$setter($this->_tplControl->evaluateExpression($value[1]));
                                        }
                                        break;
                                case self::CONFIG_TEMPLATE:
                                        $setter='set'.$name;
                                        $component->$setter($value[1]);
                                        break;
                                case self::CONFIG_ASSET:                // asset URL
                                        $setter='set'.$name;
                                        $url=$this->publishFilePath($this->_contextPath.DIRECTORY_SEPARATOR.$value[1]);
                                        $component->$setter($url);
                                        break;
                                case self::CONFIG_PARAMETER:            // application parameter
                                        $setter='set'.$name;
                                        $component->$setter($this->getApplication()->getParameters()->itemAt($value[1]));
                                        break;
                                case self::CONFIG_LOCALIZATION:
                                        $setter='set'.$name;
                                        $component->$setter(Prado::localize($value[1]));
                                        break;
                                default:        // an error if reaching here
                                        throw new TConfigurationException('template_tag_unexpected',$name,$value[1]);
                                        break;
                        }
                }
                else
                {
                        $setter='set'.$name;
                        $component->$setter($value);
                }
        }

        /**
         * Configures a subproperty for a component.
         * @param TComponent component to be configured
         * @param string subproperty name
         * @param mixed subproperty initial value
         */
        protected function configureSubProperty($component,$name,$value)
        {
                if(is_array($value))
                {
                        switch($value[0])
                        {
                                case self::CONFIG_DATABIND:             // databinding
                                        $component->bindProperty($name,$value[1]);
                                        break;
                                case self::CONFIG_EXPRESSION:           // expression
                                        if($component instanceof TControl)
                                                $component->autoBindProperty($name,$value[1]);
                                        else
                                                $component->setSubProperty($name,$this->_tplControl->evaluateExpression($value[1]));
                                        break;
                                case self::CONFIG_TEMPLATE:
                                        $component->setSubProperty($name,$value[1]);
                                        break;
                                case self::CONFIG_ASSET:                // asset URL
                                        $url=$this->publishFilePath($this->_contextPath.DIRECTORY_SEPARATOR.$value[1]);
                                        $component->setSubProperty($name,$url);
                                        break;
                                case self::CONFIG_PARAMETER:            // application parameter
                                        $component->setSubProperty($name,$this->getApplication()->getParameters()->itemAt($value[1]));
                                        break;
                                case self::CONFIG_LOCALIZATION:
                                        $component->setSubProperty($name,Prado::localize($value[1]));
                                        break;
                                default:        // an error if reaching here
                                        throw new TConfigurationException('template_tag_unexpected',$name,$value[1]);
                                        break;
                        }
                }
                else
                        $component->setSubProperty($name,$value);
        }

        /**
         * Parses a template string.
         *
         * This template parser recognizes five types of data:
         * regular string, well-formed component tags, well-formed property tags, directives, and expressions.
         *
         * The parsing result is returned as an array. Each array element can be of three types:
         * - a string, 0: container index; 1: string content;
         * - a component tag, 0: container index; 1: component type; 2: attributes (name=>value pairs)
         * If a directive is found in the template, it will be parsed and can be
         * retrieved via {@link getDirective}, which returns an array consisting of
         * name-value pairs in the directive.
         *
         * Note, attribute names are treated as case-insensitive and will be turned into lower cases.
         * Component and directive types are case-sensitive.
         * Container index is the index to the array element that stores the container object.
         * If an object has no container, its container index is -1.
         *
         * @param string the template string
         * @throws TConfigurationException if a parsing error is encountered
         */
        protected function parse($input)
        {
                $input=$this->preprocess($input);
                $tpl=&$this->_tpl;
                $n=preg_match_all(self::REGEX_RULES,$input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
                $expectPropEnd=false;
                $textStart=0;
        $stack=array();
                $container=-1;
                $matchEnd=0;
                $c=0;
                $this->_directive=null;
                try
                {
                        for($i=0;$i<$n;++$i)
                        {
                                $match=&$matches[$i];
                                $str=$match[0][0];
                                $matchStart=$match[0][1];
                                $matchEnd=$matchStart+strlen($str)-1;
                                if(strpos($str,'<com:')===0)    // opening component tag
                                {
                                        if($expectPropEnd)
                                                continue;
                                        if($matchStart>$textStart)
                                                $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
                                        $textStart=$matchEnd+1;
                                        $type=$match[1][0];
                                        $attributes=$this->parseAttributes($match[2][0],$match[2][1]);
                                        $this->validateAttributes($type,$attributes);
                                        $tpl[$c++]=array($container,$type,$attributes);
                                        if($str[strlen($str)-2]!=='/')  // open tag
                                        {
                                                array_push($stack,$type);
                                                $container=$c-1;
                                        }
                                }
                                else if(strpos($str,'</com:')===0)      // closing component tag
                                {
                                        if($expectPropEnd)
                                                continue;
                                        if($matchStart>$textStart)
                                                $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
                                        $textStart=$matchEnd+1;
                                        $type=$match[1][0];

                                        if(empty($stack))
                                                throw new TConfigurationException('template_closingtag_unexpected',"</com:$type>");

                                        $name=array_pop($stack);
                                        if($name!==$type)
                                        {
                                                $tag=$name[0]==='@' ? '</prop:'.substr($name,1).'>' : "</com:$name>";
                                                throw new TConfigurationException('template_closingtag_expected',$tag);
                                        }
                                        $container=$tpl[$container][0];
                                }
                                else if(strpos($str,'<%@')===0) // directive
                                {
                                        if($expectPropEnd)
                                                continue;
                                        if($matchStart>$textStart)
                                                $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
                                        $textStart=$matchEnd+1;
                                        if(isset($tpl[0]) || $this->_directive!==null)
                                                throw new TConfigurationException('template_directive_nonunique');
                                        $this->_directive=$this->parseAttributes($match[4][0],$match[4][1]);
                                }
                                else if(strpos($str,'<%')===0)  // expression
                                {
                                        if($expectPropEnd)
                                                continue;
                                        if($matchStart>$textStart)
                                                $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
                                        $textStart=$matchEnd+1;
                                        $literal=trim($match[5][0]);
                                        if($str[2]==='=')       // expression
                                                $tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,$literal));
                                        else if($str[2]==='%')  // statements
                                                $tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_STATEMENTS,$literal));
                                        else if($str[2]==='#')
                                                $tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_DATABINDING,$literal));
                                        else if($str[2]==='$')
                                                $tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"\$this->getApplication()->getParameters()->itemAt('$literal')"));
                                        else if($str[2]==='~')
                                                $tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"\$this->publishFilePath('$this->_contextPath/$literal')"));
                                        else if($str[2]==='/')
                                                $tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"dirname(\$this->getApplication()->getRequest()->getApplicationUrl()).'/$literal'"));
                                        else if($str[2]==='[')
                                        {
                                                $literal=strtr(trim(substr($literal,0,strlen($literal)-1)),array("'"=>"\'","\\"=>"\\\\"));
                                                $tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"Prado::localize('$literal')"));
                                        }
                                }
                                else if(strpos($str,'<prop:')===0)      // opening property
                                {
                                        if(strrpos($str,'/>')===strlen($str)-2)  //subproperties
                                        {
                                                if($expectPropEnd)
                                                        continue;
                                                if($matchStart>$textStart)
                                                        $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
                                                $textStart=$matchEnd+1;
                                                $prop=strtolower($match[6][0]);
                                                $attrs=$this->parseAttributes($match[7][0],$match[7][1]);
                                                $attributes=array();
                                                foreach($attrs as $name=>$value)
                                                        $attributes[$prop.'.'.$name]=$value;
                                                $type=$tpl[$container][1];
                                                $this->validateAttributes($type,$attributes);
                                                foreach($attributes as $name=>$value)
                                                {
                                                        if(isset($tpl[$container][2][$name]))
                                                                throw new TConfigurationException('template_property_duplicated',$name);
                                                        $tpl[$container][2][$name]=$value;
                                                }
                                        }
                                        else  // regular property
                                        {
                                                $prop=strtolower($match[3][0]);
                                                array_push($stack,'@'.$prop);
                                                if(!$expectPropEnd)
                                                {
                                                        if($matchStart>$textStart)
                                                                $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
                                                        $textStart=$matchEnd+1;
                                                        $expectPropEnd=true;
                                                }
                                        }
                                }
                                else if(strpos($str,'</prop:')===0)     // closing property
                                {
                                        $prop=strtolower($match[3][0]);
                                        if(empty($stack))
                                                throw new TConfigurationException('template_closingtag_unexpected',"</prop:$prop>");
                                        $name=array_pop($stack);
                                        if($name!=='@'.$prop)
                                        {
                                                $tag=$name[0]==='@' ? '</prop:'.substr($name,1).'>' : "</com:$name>";
                                                throw new TConfigurationException('template_closingtag_expected',$tag);
                                        }
                                        if(($last=count($stack))<1 || $stack[$last-1][0]!=='@')
                                        {
                                                if($matchStart>$textStart)
                                                {
                                                        $value=substr($input,$textStart,$matchStart-$textStart);
                                                        if(substr($prop,-8,8)==='template')
                                                                $value=$this->parseTemplateProperty($value,$textStart);
                                                        else
                                                                $value=$this->parseAttribute($value);
                                                        if($container>=0)
                                                        {
                                                                $type=$tpl[$container][1];
                                                                $this->validateAttributes($type,array($prop=>$value));
                                                                if(isset($tpl[$container][2][$prop]))
                                                                        throw new TConfigurationException('template_property_duplicated',$prop);
                                                                $tpl[$container][2][$prop]=$value;
                                                        }
                                                        else    // a property for the template control
                                                                $this->_directive[$prop]=$value;
                                                        $textStart=$matchEnd+1;
                                                }
                                                $expectPropEnd=false;
                                        }
                                }
                                else if(strpos($str,'<!--')===0)        // comments
                                {
                                        if($expectPropEnd)
                                                throw new TConfigurationException('template_comments_forbidden');
                                        if($matchStart>$textStart)
                                                $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
                                        $textStart=$matchEnd+1;
                                }
                                else
                                        throw new TConfigurationException('template_matching_unexpected',$match);
                        }
                        if(!empty($stack))
                        {
                                $name=array_pop($stack);
                                $tag=$name[0]==='@' ? '</prop:'.substr($name,1).'>' : "</com:$name>";
                                throw new TConfigurationException('template_closingtag_expected',$tag);
                        }
                        if($textStart<strlen($input))
                                $tpl[$c++]=array($container,substr($input,$textStart));
                }
                catch(Exception $e)
                {
                        if(($e instanceof TException) && ($e instanceof TTemplateException))
                                throw $e;
                        if($matchEnd===0)
                                $line=$this->_startingLine+1;
                        else
                                $line=$this->_startingLine+count(explode("\n",substr($input,0,$matchEnd+1)));
                        $this->handleException($e,$line,$input);
                }

                if($this->_directive===null)
                        $this->_directive=array();

                // optimization by merging consecutive strings, expressions, statements and bindings
                $objects=array();
                $parent=null;
                $merged=array();
                foreach($tpl as $id=>$object)
                {
                        if(isset($object[2]) || $object[0]!==$parent)
                        {
                                if($parent!==null)
                                {
                                        if(count($merged[1])===1 && is_string($merged[1][0]))
                                                $objects[$id-1]=array($merged[0],$merged[1][0]);
                                        else
                                                $objects[$id-1]=array($merged[0],new TCompositeLiteral($merged[1]));
                                }
                                if(isset($object[2]))
                                {
                                        $parent=null;
                                        $objects[$id]=$object;
                                }
                                else
                                {
                                        $parent=$object[0];
                                        $merged=array($parent,array($object[1]));
                                }
                        }
                        else
                                $merged[1][]=$object[1];
                }
                if($parent!==null)
                {
                        if(count($merged[1])===1 && is_string($merged[1][0]))
                                $objects[$id]=array($merged[0],$merged[1][0]);
                        else
                                $objects[$id]=array($merged[0],new TCompositeLiteral($merged[1]));
                }
                $tpl=$objects;
                return $objects;
        }

        /**
         * Parses the attributes of a tag from a string.
         * @param string the string to be parsed.
         * @return array attribute values indexed by names.
         */
        protected function parseAttributes($str,$offset)
        {
                if($str==='')
                        return array();
                $pattern='/([\w\.]+)\s*=\s*(\'.*?\'|".*?"|<%.*?%>)/msS';
                $attributes=array();
                $n=preg_match_all($pattern,$str,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
                for($i=0;$i<$n;++$i)
                {
                        $match=&$matches[$i];
                        $name=strtolower($match[1][0]);
                        if(isset($attributes[$name]))
                                throw new TConfigurationException('template_property_duplicated',$name);
                        $value=$match[2][0];
                        if(substr($name,-8,8)==='template')
                        {
                                if($value[0]==='\'' || $value[0]==='"')
                                        $attributes[$name]=$this->parseTemplateProperty(substr($value,1,strlen($value)-2),$match[2][1]+1);
                                else
                                        $attributes[$name]=$this->parseTemplateProperty($value,$match[2][1]);
                        }
                        else
                        {
                                if($value[0]==='\'' || $value[0]==='"')
                                        $attributes[$name]=$this->parseAttribute(substr($value,1,strlen($value)-2));
                                else
                                        $attributes[$name]=$this->parseAttribute($value);
                        }
                }
                return $attributes;
        }

        protected function parseTemplateProperty($content,$offset)
        {
                $line=$this->_startingLine+count(explode("\n",substr($this->_content,0,$offset)))-1;
                return array(self::CONFIG_TEMPLATE,new TTemplate($content,$this->_contextPath,$this->_tplFile,$line,false));
        }

        /**
         * Parses a single attribute.
         * @param string the string to be parsed.
         * @return array attribute initialization
         */
        protected function parseAttribute($value)
        {
                if(($n=preg_match_all('/<%[#=].*?%>/msS',$value,$matches,PREG_OFFSET_CAPTURE))>0)
                {
                        $isDataBind=false;
                        $textStart=0;
                        $expr='';
                        for($i=0;$i<$n;++$i)
                        {
                                $match=$matches[0][$i];
                                $token=$match[0];
                                $offset=$match[1];
                                $length=strlen($token);
                                if($token[2]==='#')
                                        $isDataBind=true;
                                if($offset>$textStart)
                                        $expr.=".'".strtr(substr($value,$textStart,$offset-$textStart),array("'"=>"\\'","\\"=>"\\\\"))."'";
                                $expr.='.('.substr($token,3,$length-5).')';
                                $textStart=$offset+$length;
                        }
                        $length=strlen($value);
                        if($length>$textStart)
                                $expr.=".'".strtr(substr($value,$textStart,$length-$textStart),array("'"=>"\\'","\\"=>"\\\\"))."'";
                        if($isDataBind)
                                return array(self::CONFIG_DATABIND,ltrim($expr,'.'));
                        else
                                return array(self::CONFIG_EXPRESSION,ltrim($expr,'.'));
                }
                else if(preg_match('/\\s*(<%~.*?%>|<%\\$.*?%>|<%\\[.*?\\]%>)\\s*/msS',$value,$matches) && $matches[0]===$value)
                {
                        $value=$matches[1];
                        if($value[2]==='~') // a URL
                                return array(self::CONFIG_ASSET,trim(substr($value,3,strlen($value)-5)));
                        else if($value[2]==='[')
                                return array(self::CONFIG_LOCALIZATION,trim(substr($value,3,strlen($value)-6)));
                        else if($value[2]==='$')
                                return array(self::CONFIG_PARAMETER,trim(substr($value,3,strlen($value)-5)));
                }
                else
                        return $value;
        }

        protected function validateAttributes($type,$attributes)
        {
                Prado::using($type);
                if(($pos=strrpos($type,'.'))!==false)
                        $className=substr($type,$pos+1);
                else
                        $className=$type;
                $class=new TReflectionClass($className);
                if(is_subclass_of($className,'TControl') || $className==='TControl')
                {
                        foreach($attributes as $name=>$att)
                        {
                                if(($pos=strpos($name,'.'))!==false)
                                {
                                        // a subproperty, so the first segment must be readable
                                        $subname=substr($name,0,$pos);
                                        if(!$class->hasMethod('get'.$subname))
                                                throw new TConfigurationException('template_property_unknown',$type,$subname);
                                }
                                else if(strncasecmp($name,'on',2)===0)
                                {
                                        // an event
                                        if(!$class->hasMethod($name))
                                                throw new TConfigurationException('template_event_unknown',$type,$name);
                                        else if(!is_string($att))
                                                throw new TConfigurationException('template_eventhandler_invalid',$type,$name);
                                }
                                else
                                {
                                        // a simple property
                                        if(!$class->hasMethod('set'.$name))
                                        {
                                                if($class->hasMethod('get'.$name))
                                                        throw new TConfigurationException('template_property_readonly',$type,$name);
                                                else
                                                        throw new TConfigurationException('template_property_unknown',$type,$name);
                                        }
                                        else if(is_array($att) && $att[0]!==self::CONFIG_EXPRESSION)
                                        {
                                                if(strcasecmp($name,'id')===0)
                                                        throw new TConfigurationException('template_controlid_invalid',$type);
                                                else if(strcasecmp($name,'skinid')===0)
                                                        throw new TConfigurationException('template_controlskinid_invalid',$type);
                                        }
                                }
                        }
                }
                else if(is_subclass_of($className,'TComponent') || $className==='TComponent')
                {
                        foreach($attributes as $name=>$att)
                        {
                                if(is_array($att) && ($att[0]===self::CONFIG_DATABIND))
                                        throw new TConfigurationException('template_databind_forbidden',$type,$name);
                                if(($pos=strpos($name,'.'))!==false)
                                {
                                        // a subproperty, so the first segment must be readable
                                        $subname=substr($name,0,$pos);
                                        if(!$class->hasMethod('get'.$subname))
                                                throw new TConfigurationException('template_property_unknown',$type,$subname);
                                }
                                else if(strncasecmp($name,'on',2)===0)
                                        throw new TConfigurationException('template_event_forbidden',$type,$name);
                                else
                                {
                                        // id is still alowed for TComponent, even if id property doesn't exist
                                        if(strcasecmp($name,'id')!==0 && !$class->hasMethod('set'.$name))
                                        {
                                                if($class->hasMethod('get'.$name))
                                                        throw new TConfigurationException('template_property_readonly',$type,$name);
                                                else
                                                        throw new TConfigurationException('template_property_unknown',$type,$name);
                                        }
                                }
                        }
                }
                else
                        throw new TConfigurationException('template_component_required',$type);
        }

        /**
         * @return array list of included external template files
         */
        public function getIncludedFiles()
        {
                return $this->_includedFiles;
        }

        /**
         * Handles template parsing exception.
         * This method rethrows the exception caught during template parsing.
         * It adjusts the error location by giving out correct error line number and source file.
         * @param Exception template exception
         * @param int line number
         * @param string template string if no source file is used
         */
        protected function handleException($e,$line,$input=null)
        {
                $srcFile=$this->_tplFile;

                if(($n=count($this->_includedFiles))>0) // need to adjust error row number and file name
                {
                        for($i=$n-1;$i>=0;--$i)
                        {
                                if($this->_includeAtLine[$i]<=$line)
                                {
                                        if($line<$this->_includeAtLine[$i]+$this->_includeLines[$i])
                                        {
                                                $line=$line-$this->_includeAtLine[$i]+1;
                                                $srcFile=$this->_includedFiles[$i];
                                                break;
                                        }
                                        else
                                                $line=$line-$this->_includeLines[$i]+1;
                                }
                        }
                }
                $exception=new TTemplateException('template_format_invalid',$e->getMessage());
                $exception->setLineNumber($line);
                if(!empty($srcFile))
                        $exception->setTemplateFile($srcFile);
                else
                        $exception->setTemplateSource($input);
                throw $exception;
        }

        /**
         * Preprocesses the template string by including external templates
         * @param string template string
         * @return string expanded template string
         */
        protected function preprocess($input)
        {
                if($n=preg_match_all('/<%include(.*?)%>/',$input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE))
                {
                        for($i=0;$i<$n;++$i)
                        {
                                $filePath=Prado::getPathOfNamespace(trim($matches[$i][1][0]),TTemplateManager::TEMPLATE_FILE_EXT);
                                if($filePath!==null && is_file($filePath))
                                        $this->_includedFiles[]=$filePath;
                                else
                                {
                                        $errorLine=count(explode("\n",substr($input,0,$matches[$i][0][1]+1)));
                                        $this->handleException(new TConfigurationException('template_include_invalid',trim($matches[$i][1][0])),$errorLine,$input);
                                }
                        }
                        $base=0;
                        for($i=0;$i<$n;++$i)
                        {
                                $ext=file_get_contents($this->_includedFiles[$i]);
                                $length=strlen($matches[$i][0][0]);
                                $offset=$base+$matches[$i][0][1];
                                $this->_includeAtLine[$i]=count(explode("\n",substr($input,0,$offset)));
                                $this->_includeLines[$i]=count(explode("\n",$ext));
                                $input=substr_replace($input,$ext,$offset,$length);
                                $base+=strlen($ext)-$length;
                        }
                }

                return $input;
        }
}