<?php
// Modification log
// (date)       (author)    (activity/purpose)
// 03.08.03     T.Gildseth  Added a default handler.
// 03.08.03     T.Gildseth  Made the default handler retain the parsed documents doctype.
// 22.08.03     T.Gildseth  Added and updated comments to comply with phpdoc(phpdoc.de)
// 22.08.03     T.Gildseth  Added assertions
//
//Todo
// (date)       (author)    (activity/purpose)
// 26.02.03     T. Gildseth Make the XMLparser namespace aware.
// 27.02.03     T. Gildseth Implement all handlers

assert_options(ASSERT_WARNING0);
require_once 
'classes/XML/class.XmlNode.php';

/**
 * This class parses an XML document, and builds a tree of XmlNodes to
 * keep the result in. It requires the existence of the expat parser in PHP.
 *
 * @author Tommy Gildseth <tommy@akili.no>;
 * @author Akili AS <post@akili.no>;
 * @copyright Akili AS, February 2003
 * @version 0.8
 * @access public
 * @package XML
 */
class XmlParser
    
{
    
//{{{ Class variables
    /**
     * Reference to the rootnode of the parsed XML tree.
     * @access private
     */
    
var $rootNode;

    
/**
     * The documents processing instruction
     * @access public
     */
    
var $doctype '';
    
/**
     * An instance of the expat parser.
     * @access private
     */
    
var $xmlParser;

    
/**
     * The parsing stack.
     * @access private
     */
    
var $stack;

    
/**
     * Index of current top level element in the stack
     * @access private
     */
    
var $stackIndex 0;
    
//}}}

    //{{{ Constructor
    /**
     * Class constructor
     *
     * @access public
     */
    
function XmlParser( )
        {} 
//}}}end constructor


    //{{{ setEncoding( String ) - US-ASCII, ISO-8859-1 and UTF-8 supported
    /**
     * Set the character encoding the parser should use.
     *
     * This function creates a new instance of the expat parser object, so it's not
     * possible to change the character encoding after parsing.
     * US-ASCII, ISO-8859-1 and UTF-8 supported
     *
     * @todo implement a wrapper for the utf8_enocde/decode functions in the
     *       expat parser
     * @param string $encoding
     *      Which character encoding the parser should use. Ie ISO-8859-1
     *      (default), US-ASCII, UTF-8
     * @access public
     */
    
function setEncoding$encoding )
        {
        
$this->_createParser($encoding);
        } 
//}}}end method setEncoding


    //{{{ parseString(String)
    /**
     * Parse the content of xmlDoc as a XML document.
     *
     * @param string $xmlDoc
     *      The XML document to be parsed.
     * @access public
     */
    
function parseString$xmlDoc )
        {
        if (!isset(
$this->xmlParser))
            
$this->_createParser();

        
xml_parse($this->xmlParser$xmlDoc);

        if (!
assert ('XML_ERROR_NONE == xml_get_error_code($this->xmlParser)'))
            {
            
$errorMsg xml_error_string(xml_get_error_code($this->xmlParser));
            echo 
'An error occured while parsing this XML document in XmlParser::parseString:<br/>';
            echo 
htmlspecialchars($xmlDoc);
            echo 
'<p/>The error was: '.$errorMsg.'<p/>';
            
$this->_printStackTrace();
            }
        } 
//}}}end method parseString


    //{{{ parseFile(String)
    /**
     * Parse the content of the file named in $filename as a XML document.
     *
     * @param string $filename
     *      Filename of XML document to be parsed.
     * @access public
     */
    
function parseFile$filename )
        {
        if (!
assert ('is_file($filename)'))
            {
            
$errorMsg xml_error_string(xml_get_error_code($this->xmlParser));
            echo 
'File '.$filename.' does not exist in call to XmlParser::parseFile:<br/>';
            
$this->_printStackTrace();
            return;
            }

        
$fp fopen($filename'r');
        
$file fread($fpfilesize($filename));
        
fclose($fp);

        
$this->parseString($file);
        } 
//}}}end method parseFile


    //{{{ getRootNode
    /**
     * Get a reference to the rootnode of the XML tree after parsing a XML document.
     *
     * @return object XmlNode
     *      A reference to the rootnode of the XML tree.
     * @access public
     */
    
function &getRootNode( )
        {
        return 
$this->rootNode;
        } 
//}}}end method getRootNode


    //{{{ getNodeAt(String)
    /**
     * Get the node at the location given in the $path parameter.
     *
     * The parameter to this function should be formated according to the W3C's
     * XPath specification. http://www.w3.org/TR/xpath for more information.
     * Only simple paths can be used.
     * Some examples:<br>
     * /<br>.<br>childOfRoot/aNode[2]/childNode<br>childOfRoot/anode/childNode<br> etc.
     * NOTE: You can not specify the name of the root node in the path. ie
     * /root/node/node2 will not work, but node/node2/ will.
     *
     * @param string $path
     *      The path to the node to retrieve.
     * @access public
     */
    
function &getNodeAt$path )
        {
        return 
$this->rootNode->getChild($path);
        } 
//}}}end method getNodeAt


    //{{{ _characterHandler(Expat parser, String)
    /**
     * Character handler. See expat docs for details.
     *
     * @param resource $parser
     *      A reference to teh XML parser calling the handler
     * @param string $data
     *      The character data as a string
     * @see <a href="http://www.php.net/xml">http://www.php.net/xml</a>
     * @access private
     */
    
function _characterHandler$parser,  $data )
        {
        if (
trim($data) != '' ||  trim($this->stack[$this->stackIndex]->getValue()) != '')
            
$this->stack[$this->stackIndex]->appendValue($data);
        } 
//}}}end method _characterHandler


    //{{{ _startElementHandler(Expat parser, String, Array)
    /**
     * Start element handler. See expat docs for details.
     *
     * @param resource $parser
     *      A reference to the XML parser calling this handler.
     * @param string $name
     *      Name of the element for which this handler is called.
     * @param array $attribs
     *      An associative array with the elements attributes (if any). The keys of this
     *      array are the attribute names, the values are the attribute values.
     * @see <a href="http://www.php.net/xml">http://www.php.net/xml</a>
     * @access private
     */
    
function _startElementHandler$parser,  $name,  $attribs )
        {
        ++
$this->stackIndex;

        unset(
$this->stack[$this->stackIndex]);
        
$this->stack[$this->stackIndex] = new XmlNode($name$attribs);
        if (!isset(
$this->rootNode))
            {
            
$this->rootNode = &$this->stack[$this->stackIndex];
            }
        else
            {
            
$this->stack[$this->stackIndex-1]->addChild('.'$this->stack[$this->stackIndex]);
            }
        } 
//}}}end method _startElementHandler


    //{{{ _endElementHandler(Expat parser, String)
    /**
     * End element handler. See expat docs for details.
     * @param resource $parser
     *      A reference to the XML parser calling the handler
     * @param string $name
     *      Contains the name of the closing element which this handler is called for.
     * @see <a href="http://www.php.net/xml">http://www.php.net/xml</a>
     * @access private
     */
    
function _endElementHandler$parser,  $name )
        {
        if (!
assert ('$this->stackIndex > -1'))
            {
            echo 
'Stack underflow in XmlParser::_endElementHandler:<br/>';
            
$this->_printStackTrace();
            return;
            }
        --
$this->stackIndex;
        } 
//}}}end method _endElementHandler


    //{{{ _piHandler(Expat parser, String)
    /**
     * FIXME: NOT IMPLEMENTED
     */
    
function _piHandler$parser$target$data )
        {
        
/*echo "This is output from an unimplemented method (_piHandler) in class.XmlParser.php<br/>\n";
        print_r($target);
        print_r($data);*/
        
}//}}}end method _piHandler

    //{{{ _notDeclHandler(Expat parser, String)
    /**
     * End element handler. See expat docs for details.(This method is not yet supported)
     * @param $parser
     *      The first parameter, parser, is a reference to the XML parser calling the handler.
     * @param $notation_name
     *      This is the notation's name
     * @param $base
     *      This is the base for resolving the system identifier (system_id) of the
     *      notation declaration. Currently this parameter will always be set to an
     *      empty string.
     * @ param $system_id System identifier of the external notation declaration.
     * @ param $public_id Public identifier of the external notation declaration.
     * @see <a href="http://www.php.net/xml">http://www.php.net/xml</a>
     * @access private
     */
    
function _notDeclHandler$parser$notation_name$base$system_id$public_id)
        {
        
/*echo "This is output from an unimplemented method (_notDeclHandler) in class.XmlParser.php<br/>\n";
        print_r($notation_name);
        print_r($base);
        print_r($system_id);
        print_r($public_id);*/
        
}//}}}end method _notDeclHandler


    //{{{ _defaultHandler(Expat parser, String)
    /**
     * End element handler. See expat docs for details.
     * @param resource $parser
     *      The first parameter, parser, is a reference to the XML parser calling the handler.
     * @param string $data
     * @see <a href="http://www.php.net/xml">http://www.php.net/xml</a>
     * @access private
     */
    
function _defaultHandler$parser$data )
        {
        if (
$this->stackIndex == 0)
            {
            
$this->doctype .= $data;
            }
        else
            {
            if (
trim($data) != '')
                {
                
$this->stack[$this->stackIndex]->appendValue($data);
                }
            }
        }
//}}}end method _defaultHandler


    //{{{ _createParser(String)
    /**
     * Create an expat XML parser, using the supplied characterset
     *
     * @param string $encoding The characterset of the document to be parsed.
     */
    
function _createParser$encoding='ISO-8859-1' )
        {
        
$this->xmlParser xml_parser_create($encoding);
        
xml_set_object($this->xmlParser, &$this);
        
xml_parser_set_option($this->xmlParserXML_OPTION_CASE_FOLDING0);
        
xml_set_element_handler($this->xmlParser,
                                
'_startElementHandler',
                                
'_endElementHandler');
        
xml_set_character_data_handler($this->xmlParser'_characterHandler');
        
xml_set_processing_instruction_handler$this->xmlParser'_piHandler');
        
xml_set_notation_decl_handler$this->xmlParser'_notDeclHandler');
        
xml_set_default_handler $this->xmlParser'_defaultHandler');
        } 
//}}}end method _createParser


    //{{{ reset
    /**
     * Remove the contents of this class, and make it ready for reuse.
     */
    
function reset()
        {
        
$this->rootNode null;
        
$this->stack null;
        
$this->xmlParser null;
        
$stackIndex 0;
        } 
//}}}end method reset

    //{{{_printStackTrace
    /**
     * Prints a stacktrace of the callstack
     *
     * @access private
     */
    
function _printStackTrace( )
        {
        
$dbt debug_backtrace();
        echo 
'Complete trace of callstack:<br/>';
        
array_shift($dbt);
        foreach(
$dbt as $eachCall)
            {
            echo 
$eachCall['function'].' called from '.$eachCall['file'].':';
            echo 
$eachCall['line'].'<br/>';
            }
        echo 
'<p/>';
        }
//}}}end function _printStackTrace
    
}
?>