<?PHP
/**
 * class to parse an HTML document or patTemplate and extract all
 * patForms elements. They will be replaced by placeholders.
 *
 * $Id: Parser.php 6724 2006-04-02 13:40:42Z chregu $
 *
 * @author		Stephan Schmidt <schst@php-tools.net>
 * @package		patForms
 * @subpackage	Parser
 * @license		LGPL
 * @copyright	PHP Application Tools <http://www.php-tools.net>
 */

/**
 * file does not exist
 */
define( "PATFORMS_PARSER_ERROR_FILE_NOT_FOUND", 100000 );
 
/**
 * file could not be created
 */
define( "PATFORMS_PARSER_ERROR_FILE_NOT_CREATED", 100001 );

/**
 * element cannot be serialized
 */
define( "PATFORMS_PARSER_ERROR_ELEMENT_NOT_SERIALIZEABLE", 100002 );

/**
 * element cannot be serialized
 */
define( "PATFORMS_PARSER_ERROR_CACHEDIR_NOT_VALID", 100003 );

/**
 * no namespace has been declared
 */
define( "PATFORMS_PARSER_ERROR_NO_NAMESPACE", 100004 );

/**
 * static property does not exist
 */
define( "PATFORMS_PARSER_ERROR_NO_STATIC_PROPERTY", 100005 );

/**
 * basedir is not valid
 */
define( "PATFORMS_PARSER_ERROR_BASEDIR_NOT_VALID", 100006 );

/**
 * no closing tag found
 */
define( "PATFORMS_PARSER_ERROR_NO_CLOSING_TAG", 100007 );

/**
 * invalid tag found
 */
define( "PATFORMS_PARSER_ERROR_INVALID_CLOSING_TAG", 100008 );

/**
 * invalid tag found
 */
define( "PATFORMS_PARSER_ERROR_DRIVER_FILE_NOT_FOUND", 100009 );

/**
 * invalid tag found
 */
define( "PATFORMS_PARSER_ERROR_DRIVER_CLASS_NOT_FOUND", 100010 );

/**
 * form does not exist
 */
define( "PATFORMS_PARSER_ERROR_FORM_NOT_FOUND", 100011 );

/**
 * static properties
 * @var 	array
 * @access	private
 */  
$GLOBALS["_patForms_Parser"]	=	array( 
										"cacheFolder"				=>	false,
										"baseDir"					=>	false,
										"namespace"					=>	false,
										"placeholder"				=>	"{PATFORMS_ELEMENT_%s}",
										"placeholder_form_start"	=>	"{PATFORMS_FORM_%s_START}",
										"placeholder_form_end"		=>	"{PATFORMS_FORM_%s_END}",
										"placeholder_case"			=>	"upper",
										"namespaceHandlers"			=>	array()
									);


/**
 * class to parse an HTML document or patTemplate and extract all
 * patForms elements. They will be replaced by placeholders.
 *
 * It is possible to attach handlers for other namespaces.
 * The parser will delegate the tags to these handlers and
 * the return values will be used to create the form instead.
 *
 * Known issues of the parser:
 * - Currently it's only possible to parse one form per document, this will change in future versions
 *
 * @author		Stephan Schmidt <s.schmidt@metrix.de>
 * @package		patForms
 * @subpackage	Parser
 * @license		LGPL
 * @copyright	PHP Application Tools <http://www.php-tools.net>
 * @version		1.0
 * @todo		unified treatment of CData and children of the elements
 *				=> CData must be treated as children. This will allow
 *				injection of dynamic content inside any tag.
 * @todo		implement constructor to get default values from 'static properties'
 * @todo        replace double quotes with single quotes
 */
class patForms_Parser
{
   /**
	* Stores the names of all static properties that patForms_Parser will use as defaults
	* for the properties with the same name on startup.
	*
	* @access	private
	*/
	var $staticProperties	=	array(
		'cacheFolder'	=>	'setCacheDir',
		'baseDir'		=>	'setBaseDir',
		'namespace'		=>	'setNamespace',
	);

   /**
	* namespace for form elements
	* @var		string
	* @access	private
	*/
	var $_namespace			=	null;

   /**
	* namespace handlers
	* @var		string
	* @access	private
	*/
	var $_namespaceHandlers	=	array();

   /**
	* cache folder
	* @var		string
	* @access	private
	*/
	var $_cacheFolder	=	null;

   /**
	* base directory
	* @var		string
	* @access	private
	*/
	var $_baseDir	=	null;

   /**
	* placeholder template for form elements
	*
	* %s will be replaced with the name of the element
	*
	* @var		string
	* @access	private
	* @see		$_placeholder_case
	*/
	var $_placeholder	=	"{PATFORMS_ELEMENT_%s}";

   /**
	* placeholder template for start of form
	*
	* %s will be replaced with the name of the form
	*
	* @var		string
	* @access	private
	* @see		$_placeholder_case
	* @see		$_placeholder_form_end
	*/
	var $_placeholder_form_start	=	"{PATFORMS_FORM_%s_START}";

   /**
	* placeholder template for start of form
	*
	* %s will be replaced with the name of the form
	*
	* @var		string
	* @access	private
	* @see		$_placeholder_case
	* @see		$_placeholder_form_start
	*/
	var $_placeholder_form_end	=	"{PATFORMS_FORM_%s_END}";

   /**
	* case of the element name in the template
	*
	* @var		string
	* @access	private
	* @see		$_placeholder
	*/
	var $_placeholder_case	=	"upper";

   /**
	* sourcefile name
	* @var		string
	* @access	private
	*/
	var $_sourceFile;

   /**
	* outputfile name
	* @var		string
	* @access	private
	*/
	var $_outputFile;

   /**
	* form object
	* @var	object
	* @access	private
	*/
	var $_form;
	
   /**
	* name of the current form
	* @var		string
	* @access	private
	*/
	var $_currentForm = '__default';

   /**
	* form element definitions
	* @var	array
	* @access	private
	*/
	var $_elementDefinitions	=	array();

   /**
	* form attributes
	* @var	array
	* @access	private
	*/
	var $_formAttributes		=	array();
	
   /**
	* HTML code
	* @access	private
    * @var      string
	*/
	var $_html;

   /**
	* elements found during parsing process
	* @access	private
	* @var	array
	*/
	var	$_elStack	=	array();

   /**
	* cdata found during parsing process
	* @access	private
	* @var	array
	*/
	var	$_cData	=	array();

   /**
	* tag depth
	* @access	private
	* @var	integer
	*/
	var	$_depth	=	0;

   /**
	* entities that may be used in attributes
	* @var		array
	* @access	private
	*/
	var $_entities	=	array(
								'&quot;' => '"',
								'&amp;'  => '&',
								'&apos;' => '\'',
								'&gt;'   => '>',
								'&lt;'   => '<',
							);

   /**
	* constructor
	*
	* @access	public
	*/
	function patForms_Parser()
	{
		$this->__construct();	
	}
	
   /**
	* constructor
	*
	* @access	public
	*/
	function __construct()
	{
		foreach( $this->staticProperties as $staticProperty => $setMethod )
		{
			$propValue	=	patForms_Parser::getStaticProperty( $staticProperty );
			if( patErrorManager::isError( $propValue ) )
				continue;
			
			$this->$setMethod( $propValue );
		}

		/**
		 * set the placeholders
		 */
		$this->setPlaceholder( patForms_Parser::getStaticProperty( "placeholder" ), patForms_Parser::getStaticProperty( "placeholder_case" ) );
		$this->setFormPlaceholders( patForms_Parser::getStaticProperty( "placeholder_form_start" ), patForms_Parser::getStaticProperty( "placeholder_form_end" ) );
		
		// configure namespace handler
		$nsHandlers	=	&patForms_Parser::getStaticProperty( 'namespaceHandlers' );
		$namespaces	=	array_keys( $nsHandlers );
		foreach( $namespaces as $ns )
		{
			$this->addNamespace( $ns, $nsHandlers[$ns] );
		}
	}
	
   /**
	* setCacheDir
	*
	* The cache dir has to be set to utilize the caching features.
	*
	* @access	public
	* @param	mixed		path to the directory or false to disable caching
	* @return	boolean		true on success
	* @see	$_cacheFolder
	*/
    function setCacheDir( $dir )
    {
		if( $dir != false )
		{
			if( !is_dir( $dir ) || !is_writable( $dir ) )
			{
				return patErrorManager::raiseError(
					PATFORMS_PARSER_ERROR_CACHEDIR_NOT_VALID,
					"Cache folder '$dir' is either no directory or not writable.",
					"Check path and permissions"
				);
			}
		}
		
		if( isset( $this ) && is_a( $this, "patForms_Parser" ) )
		{
			$this->_cacheFolder	=	$dir;
		}
		else
		{
			patForms_Parser::setStaticProperty( "cacheFolder", $dir );
		}
		
        return  true;
    }
	
	/**
	 * Set base directory for all files.
	 *
	 * @access	public
	 * @param	mixed	path to the directory or false reset the basedir
	 * @return boolean $result	true on success
	 * @see	$_cacheFolder
	 */
    function setBaseDir( $dir )
    {
		if( $dir != false )
		{
			if( !is_dir( $dir ) )
			{
				return patErrorManager::raiseError(
					PATFORMS_PARSER_ERROR_BASEDIR_NOT_VALID,
					"Base directory '$dir' is does not exist or is no directory"
				);
			}
		}
		
		if( isset( $this ) && is_a( $this, "patForms_Parser" ) )
		{
			$this->_baseDir	=	$dir;
		}
		else
		{
			patForms_Parser::setStaticProperty( "baseDir", $dir );
		}
		
        return  true;
    }
	
   /**
	* Set the namespace for the form elements
	*
	* If this method is called statically, it will set the
	* namespace for future static calls to createFormFromTemplate()
	* and parseFile().
	*
	* If the namespace is set to null, patForms_Parser will try
	* to get the namespace declaration from the (X)HTML document
	* that is being parsed.
	*
	* That means that you should include a 
	* xmlns:myForm="http://www.php-tools.net/patForms/basic"
	* attribute in the root tag of your templates.
	* "myForm" is the namespace that you are using for your
	* patForms elements. Make sure that you are using the
	* correct URI for the attribute so it can be recognized.
	*
	* @access	public
	* @param	string	namespace
	* @see		getNamespacePrefix()
	*/
	function setNamespace( $ns )
	{
		if( isset( $this ) && is_a( $this, "patForms_Parser" ) )
		{
			$this->_namespace	=	$ns;
		}
		else
		{
			patForms_Parser::setStaticProperty( "namespace", $ns );
		}
	}
	
   /**
	* Set the placeholder template for elements.
	*
	* When parsing an HTML page that contains patForms elements
	* they will be replaced by placeholders. This method allows
	* you to set the format of the placeholders.
	* 
	* You may specify a format string like you would for
	* sprintf, with one %s that marks where the name of the
	* element will be inserted.
	*
	* @access	public
	* @param	string		placeholder
	* @param	string		flag to indicate, whether the name should be inserted in uppercase ('upper'),
	*						lowercase ('lower') or how it was specified ('keep').
	* @see		sprintf()
	* @see		setFormPlaceholders()
	*/
	function setPlaceholder( $placeholder, $case = "upper" )
	{
		if( isset( $this ) && is_a( $this, "patForms_Parser" ) )
		{
			$this->_placeholder			=	$placeholder;
			$this->_placeholder_case	=	$case;
		}
		else
		{
			patForms_Parser::setStaticProperty( "placeholder", $placeholder );
			patForms_Parser::setStaticProperty( "placeholder_case", $case );
		}
	}
	
   /**
	* Set the placeholder template for form start and end tag.
	*
	* When parsing an HTML page that contains a patForms:Form tag
	* this will be replaced by placeholders. This method allows
	* you to set the format of the placeholders.
	* 
	* You may specify a format string like you would for
	* sprintf, with one %s that marks where the name of the
	* element will be inserted.
	*
	* @access	public
	* @param	string		placeholder for the start tag
	* @param	string		placeholder for the end tag
	* @see		sprintf()
	* @see		setPlaceholder()
	*/
	function setFormPlaceholders( $start, $end )
	{
		if( isset( $this ) && is_a( $this, "patForms_Parser" ) )
		{
			$this->_placeholder_form_start	=	$start;
			$this->_placeholder_form_end	=	$end;
		}
		else
		{
			patForms_Parser::setStaticProperty( "placeholder_form_start", $start );
			patForms_Parser::setStaticProperty( "placeholder_form_end", $end );
		}
	}
	
   /**
	* add a namespace handler
	*
	* Namespace handlers can be used to include external
	* data in patForms elements.
	*
	* @access	public
	* @param	string	namespace
	* @param	object	handler
	*/
	function addNamespace( $namespace, &$handler )
	{
		if( isset( $this ) && is_a( $this, "patForms_Parser" ) )
		{
			$this->_namespaceHandlers[$namespace]	=&	$handler;
		}
		else
		{
			$ns	=&	patForms_Parser::getStaticProperty( 'namespaceHandlers' );
			$ns[$namespace]	=&	$handler;
		}
	}

   /**
	* parse a file and extract all elements
	*
	* If an outputfile is specified and the cache directory
	* has been set, two files will be created:
	* - the outputfile, containing the HTML code with placeholders for all elements
	* - a cache file that contains a serialized string with the attributes of all elements
	*
	* On the next call to parseFile() patForms_Parser will check
	* if these files exist and use them instead of parsing the sourcefile.
	*
	* @access	public
	* @param	string	$filename	filename
	* @return	boolean
	* @uses		parseString()
	*/
	function parseFile( $filename, $outputFile = null )
	{
		$this->_sourceFile	=	$filename;
		$this->_outputFile	=	$outputFile;

		if( $this->_outputFile != null )
		{
			$cache	=	$this->_checkCache();
			if( $cache )
				return	true;
		}
		
		$string	=	$this->file_get_contents( $this->_adjustFilename( $this->_sourceFile ) );
		if( $string === false )
		{
			return patErrorManager::raiseError(
				PATFORMS_PARSER_ERROR_FILE_NOT_FOUND, 
				"Sourcefile could not be read", 
				"Sourcefile: " . $this->_sourceFile 
			);
		}

		$result	=	$this->parseString( $string );
		
		if( $this->_outputFile != null && $this->_cacheFolder != null )
		{
			$success = $this->_writeHTMLToFile();
			if( patErrorManager::isError( $success ) )
				return $success;
				
			$success = $this->_writeFormToFile();
			if( patErrorManager::isError( $success ) )
				return $success;
		}
		
		return	$result;
	}

   /**
    * write the resulting HTML code to a file
	*
	* @access	private
	*/
	function _writeHTMLToFile()
	{
		return	$this->_writeToFile( $this->_adjustFilename( $this->_outputFile ), $this->getHTML() );
	}
	
   /**
    * write the form defintions to a cache file
	*
	* @access	private
	*/
	function _writeFormToFile()
	{
		$tmp		=	array(
								'attributes'	=>	$this->_formAttributes,
								'elements'		=>	$this->_elementDefinitions
							);
		$formDef	=	serialize( $tmp );
		$cacheFile	=	$this->_getFormCacheFilename();
		return	$this->_writeToFile( $cacheFile, $formDef );
	}

   /**
	* get the filename for the form cache
	*
	* @access	private
	* @return	string
	*/
	function _getFormCacheFilename()
	{
		return	$this->_cacheFolder . "/" . md5( $this->_sourceFile ) . ".form";
	}

   /**
    * adjust a filename according to the specified basedir
	*
	* @access	private
	* @param	string		filename
	* @param	string		adjusted filename
	*/
	function _adjustFilename( $filename )
	{
		if( !empty( $this->_baseDir ) )
			return	"{$this->_baseDir}/$filename";
		return $filename;
	}
	
   /**
	* write a string to a file
	*
	* I dreamed of a world, where PHP5 has already been declared as
	* stable and I could just use file_put_contents() for such an easy
	* task.
	*
	* @access	private
	* @param	string	$filename
	* @param	string	$data
	*/
	function _writeToFile( $file, $data )
	{
		$fp	=	@fopen( $file, "w" );
		if( !$fp )
		{
			return patErrorManager::raiseError(
				PATFORMS_PARSER_ERROR_FILE_NOT_CREATED, 
				"Could not create file.", 
				"File: " . $file 
			);
		}
		flock( $fp, LOCK_EX );
		fwrite( $fp, $data );
		flock( $fp, LOCK_UN );
		fclose( $fp );
		return	true;
	}
	
	
   /**
	* check, whether a cache can be used
	*
	* @access	private
	* @return	boolean		true, if the cache exists and still is valid, false otherwise
	*/
	function _checkCache()
	{
		if( empty( $this->_cacheFolder ) )
			return	false;
			
		if( !file_exists( $this->_outputFile ) )
			return	false;

		$cacheFile	=	$this->_getFormCacheFilename();
			
		if( !file_exists( $cacheFile ) )
			return	false;

		$srcFile	=	$this->_adjustFilename( $this->_sourceFile );
		$srcTime	=	filemtime( $srcFile );
			
		if( filemtime( $this->_outputFile ) < $srcTime )
			return	false;

		if( filemtime( $cacheFile ) < $srcTime )
			return	false;

		$form	=	$this->file_get_contents( $cacheFile );
		if( $form === false )
			return false;

		$tmp	=	unserialize( $form );
		$this->_formAttributes		=	$tmp['attributes'];
		$this->_elementDefinitions	=	$tmp['elements'];
		return	true;
	}
	
   /**
	* parse a string and extract all elements
	*
	* @access	public
	* @param	string	$string		html string that should be parsed
	* @return	boolean
	*/
	function parseString( $string )
	{
		$this->_elStack	=	array();
		$this->_cData	=	array();
		$this->_depth	=	0;

		// has namespace been set?
		if( $this->_namespace == null )
		{
			$ns	=	$this->getNamespacePrefix( $string );
			if( patErrorManager::isError( $ns ) )
			{
				return	$ns;
			}
			$this->setNamespace( $ns );
		}

		$knownNamespaces	=	array_merge( array( $this->_namespace ), array_keys( $this->_namespaceHandlers ) );

		$regexp	=	"/(<(\/?)([[:alnum:]]+):([[:alnum:]]+)[[:space:]]*([^>]*)>)/im";

		$tokens	=	preg_split( $regexp, $string, -1, PREG_SPLIT_DELIM_CAPTURE );

		/**
		 * the first token is always character data
		 * Though it could just be empty
		 */
		if( $tokens[0] != '' )
			$this->_characterData( $tokens[0] );
		
		$cnt	=	count( $tokens );
		$i		=	1;
		// process all tokens
		while( $i < $cnt )
		{
			$fullTag	=	$tokens[$i++];
			$closing	=	$tokens[$i++];
			$namespace	=	$tokens[$i++];
			$tagname	=	$tokens[$i++];
			$attString	=	$tokens[$i++];
			$empty		=	substr( $attString, -1 );
			$data		=	$tokens[$i++];

			/**
			 * check, whether it's a known namespace
			 */
			 if( !in_array( $namespace, $knownNamespaces ) )
			 {
			 	$this->_characterData( $fullTag );
			 	$this->_characterData( $data );
				continue;
			 }
			
			/**
			 * is it a closing tag?
			 */
			if( $closing == "/" )
			{
				$result	=	$this->_endElement( $namespace, $tagname );
				if( patErrorManager::isError( $result ) )
				{
					return	$result;
				}
				$this->_characterData( $data );
				continue;
			}

			/**
			 * Is empty or opening tag!
			 */
			$attributes		=	$this->_parseAttributes( $attString );
			$result = $this->_startElement( $namespace, $tagname, $attributes );
			if( patErrorManager::isError( $result ) )
			{
				return	$result;
			}

			/**
			 * check, if the tag is empty
			 */
			if( $empty == "/" )
			{
				$result	=	$this->_endElement( $namespace, $tagname );
				if( patErrorManager::isError( $result ) )
				{
					return	$result;
				}
			}

			$this->_characterData( $data );
		}
		
		/**
		 * check for tags that are still open
		 */
		if( $this->_depth > 0 )
		{
			$el	=	array_pop( $this->_elStack );
			return patErrorManager::raiseError(
				PATFORMS_PARSER_ERROR_NO_CLOSING_TAG,
				"No closing tag for {$el['ns']}:{$el['name']} found." 
			);
		}

		return	true;
	}

   /**
	* parse an attribute string and build an array
	*
	* @access	private
	* @param	string	attribute string
	* @param	array	attribute array
	*/
	function _parseAttributes( $string )
	{
		//	Check for trailing slash, if tag was an empty XML Tag
		if( substr( $string, -1 ) == "/" )
			$string	=	trim( substr( $string, 0, strlen( $string )-1 ) );

		$attributes	=	array();
		preg_match_all('/([a-zA-Z_\:]+)="((?:\\\.|[^"\\\])*)"/', $string, $match);
		for ($i = 0; $i < count($match[1]); $i++)
		{
			$attributes[strtolower( $match[1][$i] )] = strtr( (string)$match[2][$i], $this->_entities );
		}
		return	$attributes;
	}

   /**
	* start element handler
	*
	* @access	private
	* @param	string		namespace
	* @param	string		local part
	* @param	array		attributes
	* @see		endElement()
	*/
	function _startElement( $ns, $name, $attributes )
	{
		array_push( $this->_elStack, array(
											"ns"			=>	$ns,
											"name"			=>	$name,
											"attributes"	=>	$attributes,
											"hasChildren"	=>	false
										)
				 );

		switch( $name )
		{
			/**
			 * Build a form
			 */			
			case 'Form':
				if (isset($attributes['name'])) {
					$this->_currentForm     = $attributes['name'];
				}
				$this->_formAttributes[$this->_currentForm]	= $attributes;
				$this->_characterData( $this->_getPlaceholderForForm( $attributes['name'], 'start' ) );
				break;
		}		
		$this->_depth++;

		$this->_data[$this->_depth]	=	'';
	}

   /**
	* end element handler
	*
	* @access	private
	* @param	string		namespace
	* @param	string		local part
	* @uses		addElementDefinition() to add an element to the form
	* @uses		_callNamespaceHandler() to delegate callback to a handler
	*/
	function _endElement( $ns, $name )
	{
		$el			=	array_pop( $this->_elStack );
		$data		=	$this->_getCData();
		$this->_depth--;

		if( $el["name"] != $name || $el["ns"] != $ns )
		{
			return patErrorManager::raiseError(
				PATFORMS_PARSER_ERROR_INVALID_CLOSING_TAG,
				"Invalid closing tag {$ns}:{$name}. {$el['ns']}:{$el['name']} expected." 
			);
		}

		/**
		 * Foreign namespace has been found
		 * delegate it to the namespace handler
		 */
		if( $ns != $this->_namespace )
		{
			$result	=	$this->_callNamespaceHandler( $ns, $name, $el["attributes"], $data );
			if( $this->_depth > 0 )
			{
				$this->_addToParentTag( $result );
			}
			elseif( is_string( $result ) )
			{
				$this->_characterData( $result );
			}
			return true;
		}

		switch( $name )
		{
			/**
			 * Build a form
			 */			
			case	"Form":
				$this->_characterData( $data );
				$this->_currentForm = '__default';
				$this->_characterData( $this->_getPlaceholderForForm( $el['attributes']['name'], 'end' ) );
				break;

			/**
			 * Add an option to an Enum element
			 */
			case	"Option":
				$parent	=	array_pop( $this->_elStack );
				array_push( $this->_elStack, $parent );

				$parentName = strtolower( $parent['name'] );
				
				if( isset( $el["attributes"]["value"] ) )
				{
					if( !isset( $el['attributes']['id'] ) )
					{
						if( !isset( $parent['children'] ) )
							$parent['children'] = array();

						$cnt = count( $parent['children'] ) + 1;
						
						$el['attributes']['id'] = $parent['attributes']['name'] . '_option' . $cnt;
					}

    				$label	=	isset( $el["attributes"]["label"] ) ? $el["attributes"]["label"] : $data;
    				$id	    =	isset( $el["attributes"]["id"] ) ? $el["attributes"]["id"] : $el['attributes']['value'];
					$option	=	$el['attributes'];
					
					$option['label']	=	$label;
					$option['id']		=	$id;
				}
				else
				{
					$option	=	$data;
				}
				
				$this->_addToParentTag( $option, null, true );

                /**
                 * if it is an option of a radio group, add a placeholder
                 *
                 */
				if( $parentName == 'radiogroup' )
				{
					$this->_characterData( '{PATFORMS_ELEMENT_'.strtoupper( $parent['attributes']['name'] ).'_' . strtoupper( $el['attributes']['id'] ) . '}' );
				}
				break;

			/**
			 * Use a datasoucre
			 */
			case	"Datasource":
				switch( strtolower( $el["attributes"]["type"] ) )
				{
					/**
					 * Datasource is a callback
					 * This can either be a function or a static method
					 */
					case	"callback":
						/**
						 * get method from method or function attribute
						 */
						if( isset( $el["attributes"]["method"] ) )
							$method	=	$el["attributes"]["method"];
						elseif( isset( $el["attributes"]["function"] ) )
							$method	=	$el["attributes"]["function"];
						else
							die( "No callback method specified." );

						if( isset( $el["attributes"]["class"] ) )
						{
							$datasource	=	array( $el["attributes"]["class"], $method );
						}
						else
						{
							$datasource	=	$method;
						}
						break;
						
					/**
					 * add a custom datasource
					 */
					case	"custom":
						$datasource	=	$el["children"];
						break;

				}
				$this->_addToParentAttributes( "datasource", $datasource );
				break;

			/**
			 * Set any attribute
			 */
			case	"Attribute":
				if( isset( $el["children"] ) )
				{
					$data	=	$el["children"];
				}
				$this->_addToParentAttributes( $el["attributes"]["name"], $data );
				break;

			/**
			 * Adjust a radio-group => use the children as values
			 */
			case	"RadioGroup":
				$el['type'] = $el['name'];
				unset($el['name']);

				if( isset( $el["children"] ) ) {
					if( is_array( $el["children"] ) ) {
						$el["attributes"]["values"]		=	$el["children"];
						unset($el['children']);
					}
				}

				$renderer = null;
				
				// for the radio group, we use the string renderer as renderer, but 
				// only if a template is available at all - if there is none, we 
				// simply let the radiogroup render itself the default way.
				$template = trim( $data );
				if( !empty( $template ) ) {
					$el['renderer'] =& patForms::createRenderer( 'String' );
					$el['renderer']->setTemplate( $data );
					$el['renderer']->setPlaceholder( '{PATFORMS_ELEMENT_'.strtoupper( $el['attributes']['name'] ).'_%s}' );
				}
				
				if (count($this->_elStack) > 1) {
					$this->_addToParentTag($el, $el['attributes']['name'], true);
				} else {
					$this->addElementDefinitionByArray( $el );
				}
				$this->_characterData( $this->_getPlaceholderForElement( $el['attributes']['name'] ) );
				break;

			/**
			 * Adjust an enum => use the children as values
			 */
			case	"Enum":
			case	"Set":
				$el['type'] = $el['name'];
				unset($el['name']);

				if( isset( $el["children"] ) ) {
					if( is_array( $el["children"] ) ) {
						$el["attributes"]["values"]	=	$el["children"];
						unset($el['children']);
					}
				}
				
				if (count($this->_elStack) > 1) {
					$this->_addToParentTag($el, $el['attributes']['name'], true);
				} else {
					$this->addElementDefinitionByArray( $el );
				}

				$this->_characterData( $this->_getPlaceholderForElement( $el["attributes"]["name"] ) );
				break;

			/**
			 * No reserved value, treat it as an 
			 * element and add it to the definitions
			 */
			default:
				$el['type'] = $el['name'];
				unset($el['name']);
				
				// add a renderer to the Group
				if (strtolower($el['type']) === 'group') {
					$el['renderer'] = patForms::createRenderer('String');
					$el['renderer']->setTemplate($data);
					$el['renderer']->setPlaceholder('{'.$this->_placeholder.'}', 'name');
				}

				if (count($this->_elStack) > 1) {
					$this->_addToParentTag($el, $el['attributes']['name'], true);
				} else {
					$this->addElementDefinitionByArray( $el );
				}
				$this->_characterData( $this->_getPlaceholderForElement( $el["attributes"]["name"] ) );
				break;
		}
	}

   /**
	* call a method in a namespace handler
	*
	* @access	private
	* @param	string	namespace
	* @param	string	tag name, will be used as method name
	* @param	array	attributes of the tag
	* @param	mixed	content of the tag
	* @todo		implement error handling
	*/
	function &_callNamespaceHandler( $ns, $name, $attributes, $data )
	{
		if( !method_exists( $this->_namespaceHandlers[$ns], $name ) ) {
			die( "Unknown tag $name in namespace $ns." );
		}
		$result	=	&$this->_namespaceHandlers[$ns]->$name( $attributes, $data );
		return	$result;
	}
	
   /**
	* add an element to the element definition list
	*
	* The element definition list is used to build the form
	* and also serialized for caching
	*
	* @access	public
	* @param	string	name
	* @param	string	type
	* @param	array	attributes
	* @param	object	renderer
	* @todo		allow more than one element with the same name
	*/
	function addElementDefinition( $name, $type, $attributes, $renderer = null )
	{
		if (!isset($this->_elementDefinitions[$this->_currentForm])) {
			$this->_elementDefinitions[$this->_currentForm] = array();
		}
		$this->_elementDefinitions[$this->_currentForm][$name]	=	array(
														"type"			=>	$type,
														"attributes"	=>	$attributes,
														"renderer"		=>	&$renderer
										 			);
		return	true;
	}
	
   /**
	* add an element to the element definition list
	*
	* The element definition list is used to build the form
	* and also serialized for caching
	*
	* @access	public
	* @param	string	name
	* @param	string	type
	* @param	array	attributes
	* @param	object	renderer
	* @todo		allow more than one element with the same name
	*/
	function addElementDefinitionByArray( $def )
	{
		if (!isset($this->_elementDefinitions[$this->_currentForm])) {
			$this->_elementDefinitions[$this->_currentForm] = array();
		}
		$name = $def['attributes']['name'];
		$this->_elementDefinitions[$this->_currentForm][$name] = $def;
		return	true;
	}
	
   /**
	* cdata handler
	*
	* @access	private
	* @param	string		data
	*/
	function _characterData( $data )
	{
		if( $this->_depth == 0 ) {
			$this->_html	.=	$data;
			return	true;
		}
		$this->_data[$this->_depth]	.=	$data;
	}

   /**
	* get the character data of the element
	*
	* @access	private
	* @return	string
	*/
	function _getCData()
	{
		if( $this->_depth == 0 ) {
			return	'';
		}
		return $this->_data[$this->_depth];
	}

   /**
    * adds an attribute to the parent tag
	*
	* @access	private
	* @param	string	attribute name
	* @param	mixed	attribute value
	*/
	function _addToParentAttributes( $name, $value )
	{
		$parent	=	array_pop( $this->_elStack );
		$parent["attributes"][$name]	=	$value;
		array_push( $this->_elStack, $parent );

		return true;
	}
	
   /**
	* add child element to parant tag
	*
	* This is used to build enum lists or groups.
	* In the definition of the 
	*
	* @access	private
	* @param	mixed	child to add, normally is an array
	* @param	string	key of the child
	* @param	boolean	defines whether the element always has more the one child.
	*					If set to true the first element will be stored in an array
	* @return	boolean	success, currently always true
	*/
	function _addToParentTag( $child, $key = null, $hasMultiple = false )
	{
		// get the parent tag
		$parent	=	array_pop( $this->_elStack );
		
		// check if there already are children
		if( !$parent["hasChildren"] )
		{
			$parent["hasChildren"]	=	true;
			
			/**
			 * no key defined => just set this as only child
			 */
			if( $key == null && !$hasMultiple )
			{
				$parent["children"]	=	$child;
				
				array_push( $this->_elStack, $parent );
				return	true;
			}
			else
			{
				$parent["children"]	=	array();
			}
		}

		// if a key has been supplied
		if( $key != null )
			$parent["children"][$key]	=	$child;
		else
			array_push( $parent["children"], $child );

		array_push( $this->_elStack, $parent );
		
		return	true;
	}

   /**
    * get the name of the parent tag
	*
	* @access	private
	* @return	string	tag name
	*/
	function _getParentName()
	{
		$parent	=	array_pop( $this->_elStack );
		array_push( $this->_elStack, $parent );

		return $parent['name'];
	}

   /**
	* get the placeholder for an element
	*
	* @access	protected
	* @param	string		element name
	* @param	string		name of the placeholder template
	* @return	string		placeholder
	*/
	function _getPlaceholderForElement( $element, $template = 'placeholder' )
	{
		// adjust the case
		switch( $this->_placeholder_case )
		{
			case "upper":
				$element	=	strtoupper( $element );
				break;
			case "lower":
				$element	=	strtolower( $element );
				break;
			default:
				break;
		}
		
		return	sprintf( $this->{'_'.$template}, $element );
	}
	
   /**
	* get the placeholder for a form tag
	*
	* @access	protected
	* @param	string		name of the form
	* @param	string		type (start|end)
	* @return	string		placeholder
	*/
	function _getPlaceholderForForm( $form, $type )
	{
		// adjust the case
		switch( $this->_placeholder_case )
		{
			case 'upper':
				$form	=	strtoupper( $form );
				break;
			case 'lower':
				$form	=	strtolower( $form );
				break;
			default:
				break;
		}

		$template	=	'_placeholder_form_'.$type;
		return	sprintf( $this->$template, $form );
	}

   /**
	* get the one form element
	*
	* @access	public
	* @param	string	element name
	* @return	object	patForms element
	* @deprecated	Please use getForm() instead and fetch the elements from the form
	* @todo		fix this method! It is totally broken as patForms_Parser now may contain more than one form. Best solution would be removing the method if nobody objects
	*/
	function &getFormElement( $name )
	{
		return	$this->_form->getElement( $name );
	}

   /**
	* get the complete form object
	*
	* @access	public
	* @return	object	patForms object
	*/
	function &getForm($name = null)
	{
		if ($name === null && count($this->_elementDefinitions) === 1) {
			reset($this->_elementDefinitions);
			$name = key($this->_elementDefinitions);
		}
		
		if ($name === null) {
			if (!is_object($this->_form)) {
				require_once PATFORMS_INCLUDE_PATH . '/Collection.php';
				$this->_form = new patForms_Collection();
			}
			foreach ($this->_elementDefinitions as $formName => $elementDefinitions) {
				if ($this->_form->containsForm($formName)) {
					continue;					
				}
				$this->_form->addForm( patForms::createForm( $elementDefinitions, $this->_formAttributes[$formName] ) );
			}
			return $this->_form;
		}

		if (is_object( $this->_form[$name] )) {
			return	$this->_form[$name];
		}
		
		if (!isset($this->_elementDefinitions[$name])) {
			return patErrorManager::raiseError(PATFORMS_PARSER_ERROR_FORM_NOT_FOUND, 'The form does not exist.');
		}

		if (!isset($this->_formAttributes[$name])) {
			$this->_formAttributes[$name] = array(
													'name' => 'form'
												);
		}

		$this->_form[$name] = &patForms::createForm( $this->_elementDefinitions[$name], $this->_formAttributes[$name] );
		return	$this->_form[$name];
	}

   /**
	* get the HTML code where all form elements have been replaced with variables
	*
	* @access	public
	* @return	string	HTML code
	*/
	function getHTML()
	{
		if( $this->_html != null )
			return	$this->_html;
			
		if( $this->_outputFile == null )
			return	false;

		$this->_html	=	$this->file_get_contents( $this->_adjustFilename( $this->_outputFile ) );
		if( $this->_html === false )
		{
			return patErrorManager::raiseError(
				PATFORMS_PARSER_ERROR_FILE_NOT_FOUND, 
				"Sourcefile could not be read", 
				"Sourcefile: " . $this->_sourceFile 
			);
		}

		return	$this->_html;
	}

   /**
	* create a new parser
	*
	* Use this method to create a specialty
	* parser, that uses a driver.
	*
	* This could include a parser that is a renderer
	* at the same time.
	*
	* @static
	* @access	public
	* @param	mixed	driver name
	* @return	object patForms_Parser
	*/
	static function &createParser( $driver = null )
	{
		// not based on any driver
		if( $driver == null )
		{
			$parser	=	&new patForms_Parser;
		}
		else
		{
			$driverFile	=	dirname( __FILE__ ) . "/Parser/{$driver}.php"; 
			if( !@include_once( $driverFile ) )
			{
				return patErrorManager::raiseError(
					PATFORMS_PARSER_ERROR_DRIVER_FILE_NOT_FOUND,
					"Driver file '$driverFile' could not be loaded, file not found"
				);
			}

			$parserClass	=	"patForms_Parser_{$driver}";
			if( !class_exists( $parserClass ) )
			{
				return patErrorManager::raiseError(
					PATFORMS_PARSER_ERROR_DRIVER_CLASS_NOT_FOUND,
					"Driver file has been loaded correctly, but the according driver class '$parserClass' could not be found"
				);
			}
			$parser		=	&new $parserClass;
		}
		
		return	$parser;
	}
	
   /**
    * Create a form from an FTMPL (Form Template) file.
	*
	* This method will return a form object, that already
	* contains a reference to the parser, that can be used
	* as a renderer for the form.
	*
	* @access	public
	* @static
	* @param	string		driver for the parser, use null for the default parser
	* @param	string		form template to create the form
	* @param	string		outputfile for the resulting HTML code without form elements
	* @return	object		patForms object
	*/
	function &createFormFromTemplate( $driver, $formTemplate, $outputFile = null )
	{
		// create a new parser
		$parser		=&	patForms_Parser::createParser( $driver );
		$success	=&	$parser->parseFile( $formTemplate, $outputFile );
		if( patErrorManager::isError( $success ) )
			return $success;
	
		$form =& $parser->getForm();
		$form->setRenderer( $parser, array( 'includeElements' => true ) );
		
		return $form;
	}

   /**
	* get the namespace of patForms elements from the
	* html page.
	*
	* To make this method work, you will need an
	* xmlns:foo="http://www.php-tools.net/patForms/basic"
	* attribute in any tag of your page.
	*
	* @access	public
	* @param	string	$html		HTML document that should be parsed
	* @return	string				namespace or a patError
	*/
	function getNamespacePrefix( $html )
	{
		$regExp		=	'~xmlns:([^=]+)=["\']http://www.php-tools.net/patForms/basic["\']~im';
		
		$matches	=	array();
		$result		=	preg_match( $regExp, $html, $matches );

		if( $result )
		{
			return	$matches[1];		
		}
		return	patErrorManager::raiseError(
			PATFORMS_PARSER_ERROR_NO_NAMESPACE, 
			"No namespace for patForms declared." 
		);
	}

   /**
	* Set a static property.
	*
	* Static properties are stored in an array in a global variable,
	* until PHP5 is ready to use.
	*
	* @static
	* @param	string	property name
	* @param	string	property value
	* @see		getStaticProperty()
	*/
	function setStaticProperty( $property, &$value )
	{
		$GLOBALS["_patForms_Parser"][$property]	=	&$value;
	}

   /**
	* Get a static property.
	*
	* Static properties are stored in an array in a global variable,
	* until PHP5 is ready to use.
	*
	* @static
	* @param	string	property name
	* @return	string	property value
	* @see		setStaticProperty()
	*/
	function &getStaticProperty( $property )
	{
		if( isset( $GLOBALS["_patForms_Parser"][$property] ) )
		{
			return	$GLOBALS["_patForms_Parser"][$property];
		}
		return	patErrorManager::raiseWarning(
			PATFORMS_PARSER_ERROR_NO_STATIC_PROPERTY, 
			"Static property '$property' does not exist." 
		);
	}

   /**
	* Replacement for file_get_contents
	*
	* This is needed to make patForms work with PHP < 4.3
	*
	* @access	public
	* @static
	* @param	string	filename
	* @return	mixed	content of the file or false
	*/
	function file_get_contents( $filename )
	{
		if( !file_exists( $filename ) )
			return false;
	
		if( function_exists( "file_get_contents" ) )
		{
			return file_get_contents( $filename );
		}

		$fp	=	@fopen( $filename, "rb" );
		if( !$fp )
		{
			return false;
		}
		
		flock( $fp, LOCK_SH );
		$content	=	fread( $fp, filesize( $filename ) );
		flock( $fp, LOCK_UN );
		fclose( $fp );
		return $content;
	}

   /**
	* Replacement for is_a
	*
	* This is needed to make patForms work with PHP < 4.2
	*
	* @access	public
	* @static
	* @param	object	object to test
	* @param	string	classname
	* @return	boolean	true of object is an instance of classname or derived from this class
	*/
	function is_a( $object, $classname )
	{
		if( function_exists( "is_a" ) )
		{
			return	is_a( $object, $classname );
		}

		if( !is_object( $object ) )
			return	false;

		if( get_class( $object ) == strtolower( $classname ) )
			return	true;

		return is_subclass_of( $object, $classname );
	}
}
?>