<?php
// Modification log
// (date)       (author)    (activity/purpose)
// 25.06.03     T.Gildseth  Fixed so content and children of a node, is printed in the
//                          order of the original document.
// 04.07.03     T.Gildseth  Added "insertChild" method, for inserting a node after a
//                          specified Node.
// 04.07.03     T.Gildseth  Moved ++$this->childCount; in addChild(). It looked like a bug
// 04.07.03     T.Gildseth  Fixed a bug in addChild(). Adding a node to multiple children
//                          was not possible, due to using incorrect index in $path.
// 04.07.03     T.Gildseth  Added equal method. == only checks wether 2 objects are
//                          identical, not wether they are the same object.
// 04.07.03     T.Gildseth  Removed the $dereferencedNode = $node thing in addChild()
// 04.07.03     T.Gildseth  Split the children into 2 arrays. One for numeriaclly
//                          indexed and one indexed by node name key.
// 06.07.03     T.Gildseth  Tracked down some bugs in insertChild()
// 06.07.03     T.Gildseth  Fixed the implementation of removeChild(), so it actually
//                          works.
// 22.08.03     T.Gildseth  Added and updated comments to comply with phpdoc(phpdoc.de)
// 22.08.03     T.Gildseth  Added assertions
// 07.10.03     T.Gildseth  Fixed bug in getXmlString, caused by annoying empty() behaviour.
// 18.10.03     T.Gildseth  Moved "fold tags" below method comments (the //{{{ //}}} stuff)
// 04.01.04     T.Gildseth  Added a method for removing attributes.
// 17.03.04     T.Gildseth  Added functions is_a() and debug_backtrace() for
//                          backwards compatibility
// 20.03.04     T.Gildseth  Changed the behaviour of _parsePath, so that references to
//                          rootnode is treated as relative to the current node.
// 20.03.04     T.Gildseth  Fixed a off_by_one_bug in the treatment of xpaths.
//                          xpath indexes start at 1, not 0
//
// Todo
// (date)       (author)    (activity/purpose)
// 26.02.03     T. Gildseth Make the XML node namespace aware(or is it already?)
// 12.07.03     T. Gildseth Fix the big ugly fixme in getXmlString()

assert_options(ASSERT_WARNING0);
define('WITH_WHITESPACE'true);
define('NO_WHITESPACE'false);
define('ASSOC''assoc');
define('NUMERIC''num');
/**
 * This class represents a XML node
 *
 * @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 XmlNode
    
{
    
//{{{ Class members
    /**
     * This is the name of the XML node, the instance of this class represents.
     * @access private
     */
    
var $name;

    
/**
     * The string, if any, which appears between the starting and end element of this
     * node.
     * @access private
     */
    
var $value = array();

    
/**
     * Array of XmlNode objects, which represents the childnodes of of this node.
     * Each childnode appears exactly once in each array. One contains the nodes,
     * grouped by name as the key, and one with numeric indexes, ordered by the
     * sequence they appear in the actual source XML document.
     * The associative child array is 2d array, where the second dimention, is
     * the the nodes with the same names, ordered as they appear in the original document
     * @access private
     */
    
var $childrenAssoc = array();
    var 
$childrenNum = array();

    
/**
     * Array holding the attributes of this node
     * @access private
     */
    
var $attributes;
    
//}}}


    //{{{Constructor
    /**
     * XmlNode class constructor. Creates a new XmlNode object.
     *
     * @param string $name The nodes name
     * @param array $attributes Array containing key value pairs, representing attribute
     *                          names and values.
     * @param string $value The XML nodes value
     * @access public
     */
    
function XmlNode$name$attributes=null$value="" )
        {
        
$this->value[0] = $value;
        
$this->name $name;
        if (empty(
$attributes) || !is_array($attributes))
            
$this->attributes = array();
        else
            
$this->attributes $attributes;
        }
//}}}end constructor


    //{{{ getName
    /**
     * Accessor function for the name attribute.
     *
     * @return string Name of this node.
     * @access public
     */
    
function getName( )
        {
        return 
$this->name;
        } 
//}}}end method getName


    //{{{ setName(String)
    /**
     * Assign a new name to this XML node
     *
     * @param string $name The new name for this node
     * @access public
     */
    
function setName$name )
        {
        
$this->name $name;
        } 
//}}}end method setName


    //{{{ getValue
    /**
     * Retrieve the value of this node
     *
     * @return string Value this node contains.
     * @access public
     */
    
function getValue( )
        {
        return 
implode(' '$this->value);
        } 
//}}}end method getValue


    //{{{ setValue (String)
    /**
     * Assign a (new) value to this Node
     *
     * @param string $value The new value to assign to this XML node
     * @access public
     */
    
function setValue$value )
        {
        
$this->value[count($this->childrenNum)] = $value;
        } 
//}}}end method setValue


    //{{{ appendValue (String)
    /**
     * Append to the value of this Node
     *
     * @param string $value The new value to append to this XML node
     * @access public
     */
    
function appendValue$value )
        {
        
$lastIndex count($this->childrenNum);
        if (isset(
$this->value[$lastIndex]))
            
$this->value[$lastIndex] .= $value;
        else
            
$this->value[$lastIndex] = $value;
        } 
//}}}end method appendValue


    //{{{ getChild (String)
    /**
     * Get the childnode at the specified location.
     *
     * @param string $path The path to the node to retrieve.
     * @return object XmlNode
     *      Reference to the requested node.<br>
     *      Note: you must use the reference operator to assign the return value
     *      from this function, if you want changes you make to be retained within
     *      this Tree.
     * @access public
     */
    
function &getChild$path )
        {
        if (empty(
$path) || $path == '.')
            return 
$this;

        
$path $this->_parsePath($path);

        
//If the path contains an index ([index])
        
if ($path['index'] !== '')
            {
            --
$path['index'];
            if (isset(
$this->childrenAssoc[$path['nextNode']]) &&
                isset(
$this->childrenAssoc[$path['nextNode']][$path['index']]))
                return 
$this->childrenAssoc[$path['nextNode']][$path['index']]->getChild($path['restPath']);
            else
                return 
null;
            }
        else 
//Get nodes from all matching paths
            
{
            if (isset(
$this->childrenAssoc[$path['nextNode']]))
                {
                
$return = array();
                for (
$i 0$i count($this->childrenAssoc[$path['nextNode']]); ++$i)
                    {
                    
$return[] = &$this->childrenAssoc[$path['nextNode']][$i]->getChild($path['restPath']);
                    }
                if (
count($return) == 1)
                    {
                    
$temp = &$return[0];
                    unset(
$return);
                    
$return = &$temp;
                    }
                return 
$return;
                }
            else
                {
                return 
null;
                }
            }
        } 
//}}}end method getChild


    //{{{ getChildren
    /**
     * Retrieve an array containing all childnodes.
     *
     * @param string $type num or assoc. Specifies which childarray to return.
     * @return array
     *      Array containing all childnodes of this nodeobject.<br>
     *      Note: you must use the reference operator to assign the return value
     *      from this function, if you want changes you make to be retained within
     *      this Tree.
     * @access public
     */
    
function &getChildren$type '' )
        {
        if (
$type == 'assoc')
            return 
$this->childrenAssoc;
        else if (
$type == 'num')
            return 
$this->childrenNum;
        else
            return 
array_merge($this->childrenNum$this->childrenAssoc);

        } 
//}}}end method getChildren


    //{{{ addChild (String, XmlNode)
    /**
     * Add a new childnode to the specified node
     *
     * @param string $path
     *      The path to the node where the new node should be added.
     * @param object XmlNode &$node The node to insert
     * @access public
     *
     */
    
function addChild$path, &$node )
        {
        if (!
assert ('get_class($node) == "xmlnode"'))
            {
            echo 
'Assertion failed in XmlNode::addChild:<br/>';
            echo 
' Parameter $node is not of type XmlNode<br/>';
            echo 
'It\'s type is '.get_class($node).'/'.gettype($node).' ';
            echo 
'<p/>';
            
$this->_printStackTrace();
            }

        
//This is it, this is the node.
        
if (empty($path) || $path == '.')
            {
            
$name $node->getName();

            
$this->childrenAssoc[$name][] = &$node;
            
$this->childrenNum[] = &$node;
            return;
            }

        
$path $this->_parsePath($path);

        
$dereferencedNode =$node;
        
//If the path contains an index ([index])
        
if (!empty($path['index']))
            {
            --
$path['index'];
            if (isset(
$this->childrenAssoc[$path['nextNode']]) &&
                isset(
$this->childrenAssoc[$path['nextNode']][$path['index']]))
                
$this->childrenAssoc[$path['nextNode']][$path['index']]->addChild($path['restPath'],
                                                                                  
$dereferencedNode);
            }
        else 
//Get nodes from all matching paths
            
{
            if (isset(
$this->childrenAssoc[$path['nextNode']]))
                {
                for (
$i 0$i count($this->childrenAssoc[$path['nextNode']]); ++$i)
                    {
                    
$this->childrenAssoc[$path['nextNode']][$i]->addChild($path['restPath'],
                                                                          
$dereferencedNode);
                    }
                }
            }
        } 
//}}}end method addChild


    //{{{ insertChild (String, XmlNode)
    /**
     * Insert a new childnode at a specified location.
     *
     * @param string $after The path to the location where the new node should be inserted.
     *                      If path doesn't contain an index for last node, index 0 will be
     *                      assumed.
     * @param object XmlNode $node The node to insert
     * @access public
     *
     */
    
function insertChild$after$node )
        {
        if (!
assert ('get_class($node) == "xmlnode"'))
            {
            echo 
'Assertion failed in XmlNode::insertChild:<br/>';
            echo 
' Parameter $node is not of type XmlNode<br/>';
            echo 
'It\'s type is '.get_class($node).'/'.gettype($node).' ';
            echo 
'<p/>';
            
$this->_printStackTrace();
            }

        
$after $this->_parsePath($after);

        if (
$after['index'] === '' && empty($after['restPath']))
            {
            
$after['index'] = 0;
            }
        else if (!empty(
$index))
            {
            --
$after['index'];
            }

        
//This is it, this is the node.
        
if (empty($after['restPath']))
            {
            
$prevNode = &$this->childrenAssoc[$after['nextNode']][$after['index']];
            if (
$prevNode != null)
                {
                
$j count($this->childrenNum);
                for  (
$i 0$i $j; ++$i)
                    {
                    
//Insert after this node
                    
if ($prevNode->equals($this->childrenNum[$i]))
                        {
                        for (
$k count($this->childrenNum); $k $i+1; --$k)
                            {
                            unset(
$this->childrenNum[$k]);
                            
$this->childrenNum[$k] = &$this->childrenNum[$k-1];
                            }
                        
$name $node->getName();
                        unset(
$this->childrenNum[$i+1]);
                        
$this->childrenNum[$i+1] = &$node;
                        
$this->childrenAssoc[$name][] = &$node;
                        break;
                        }
                    }
                return 
true;
                }
            else
                {
                return 
false;
                }
            }
        
$dereferencedNode =$node;
        
//If the path contains an index ([index])
        
if (!empty($after['index']) || $after['index'] === 0)
            {
            if (isset(
$this->childrenAssoc[$after['nextNode']]) &&
                isset(
$this->childrenAssoc[$after['nextNode']][$after['index']]))
                return 
$this->childrenAssoc[$after['nextNode']][$after['index']]->insertChild($after['restPath'],
                                                                                              
$dereferencedNode);
            }
        else 
//Get nodes from all matching paths
            
{
            if (isset(
$this->childrenAssoc[$after['nextNode']]))
                {
                
$return true;
                
$j count($this->childrenAssoc[$after['nextNode']]);
                for (
$i 0$i $j; ++$i)
                    {
                    
$return $return && $this->childrenAssoc[$after['nextNode']][$i]->insertChild($after['restPath'],
                                                                                                   
$dereferencedNode);
                    }
                return 
$return;
                }
            }

        } 
//}}}end method insertChild


    //{{{ removeChild (String)
    /**
     * Removes and returns the childnode at the specified location.
     *
     * @param string $path The location of the childnode to remove.
     * @return mixed The removed XmlNode or an array of XmlNodes.
     * @access public
     */
    
function removeChild$path )
        {
        
$path $this->_parsePath($path);

        
//This is it, this is the node.
        
if (empty($path['restPath']) || $path['restPath'] == '.')
            {
            if (
$path['index'] === '')
                {
                
$return = &$this->childrenAssoc[$path['nextNode']];
                unset (
$this->childrenAssoc[$path['nextNode']]);
                }
            else
                {
                --
$path['index'];
                
$return[] = &$this->childrenAssoc[$path['nextNode']][$path['index']];
                unset (
$this->childrenAssoc[$path['nextNode']][$path['index']]);
                
$this->childrenAssoc[$path['nextNode']] = array_values($this->childrenAssoc[$path['nextNode']]);
                }

            for (
$i 0$i count($return); ++$i)
                {
                
$k count($this->childrenNum);
                for (
$j 0$j $k; ++$j)
                    {
                    if (isset(
$this->childrenNum[$j]))
                        {
                        if (
$return[$i]->equals($this->childrenNum[$j]))
                            {
                            unset (
$this->childrenNum[$j]);
                            break;
                            }
                        }
                    }
                }

            
$this->childrenNum array_values($this->childrenNum);

            if (
is_array($return) && count($return) == 1)
                {
                
$temp = &$return[0];
                unset(
$return);
                
$return = &$temp;
                }

            return 
$return;
            }

        if (
$path['index'] !== '')
            {
            if (isset(
$this->childrenAssoc[$path['nextNode']]) &&
                isset(
$this->childrenAssoc[$path['nextNode']][$path['index']]))
                
$return $this->childrenAssoc[$path['nextNode']][$path['index']]->removeChild($path['restPath']);
            }
        else 
//Get nodes from all matching paths
            
{
            if (isset(
$this->childrenAssoc[$path['nextNode']]))
                {
                
$return = array();
                
$j count($this->childrenAssoc[$path['nextNode']]);
                for (
$i 0$i $j; ++$i)
                    {
                    
$return[] = $this->childrenAssoc[$path['nextNode']][$i]->removeChild($path['restPath']);
                    }
                if (
count($return) == 1)
                    {
                    
$temp = &$return[0];
                    unset(
$return);
                    
$return = &$temp;
                    }
                }
            }
        return 
$return;
        } 
//}}}end method removeChild


    //{{{ getAttribute (String)
    /**
     * Retrieve the value of the specified attribute.
     *
     * @param string $attrName Name of the attribute to retrieve
     * @return string The attribute with the supplied name.
     * @access public
     */
    
function getAttribute$attrName )
        {
        if (isset(
$this->attributes[$attrName]))
            return 
$this->attributes[$attrName];
        else
            return 
null;
        } 
//}}}end method getAttribute


    //{{{ getAttributes
    /**
     * Get an array of all the attributes this node has.
     *
     * @return array Array containing all the attributes of this node
     * @access public
     */
    
function getAttributes( )
        {
        return 
$this->attributes;
        } 
//}}}end method getAttributes


    //{{{ addAttribute (String, String)
    /**
     * Add a new attribute to this node.
     * @param string $attrName Name of the new attribute. If this attribute allread
     *                         exist, it's value will be overwritten
     * @param string $attrValue The value to assign to the attribute
     * @access public
     */
    
function addAttribute$attrName,  $attrValue )
        {
        
$this->attributes[$attrName] = $attrValue;
        } 
//}}}end method addAttribute


    //{{{ addAttribute (String, String)
    /**
     * Remove an attribute from this node.
     * @param string $attrName Name of the attribute to be removed. If this
     *                         attribute already exist, it's value will be
     *                         overwritten
     * @access public
     */
    
function remAttribute$attrName )
        {
        if (isset(
$this->attributes[$attrName]))
            unset(
$this->attributes[$attrName]);
        } 
//}}}end method addAttribute


    //{{{ getXmlString (String)
    /**
     * Get a string representation of this XML node.
     * @param boolean $whiteSpace Indicates wether whitespace should be added.
     *                            Use WITH_WHITESPACE or NO_WHITESPACE
     * @param string $indent A string indicating the indentation level, as a string of
     *                       tab characters.
     * @return string string representation of this XmlNode and it's children.
     * @access public
     */
    
function getXmlString$whiteSpace true$indent '' )
        {
        if (!
$whiteSpace)
            {
            
$indent '';
            
$newLine '';
            }
        else
            {
            
$newLine "\n";
            }

        
$nodeName $this->name;
        
//Open tag
        
$xml $indent.'<'.$nodeName;

        
//Add attributes
        
if (count($this->attributes) > 0)
            {
            foreach (
$this->attributes as $key=>$value)
                {
                
$xml .= ' '.$key.'="'.$value.'"';
                }
            }
        
$numChildren count($this->childrenNum);

        if (
$numChildren || (isset($this->value[0]) && $this->value[0] !== ''))
            {
            
$xml .= '>'.$newLine;
            if (isset(
$this->value[0]))
                
$xml .= $indent.$this->value[0].$newLine;
            }
        else if(
$nodeName == 'textarea'/**BIG UGLY FIXME**/
            
{
            
$xml .= '></'.$nodeName.'>'.$newLine;
            return 
$xml;
            }
        else
            {
            
$xml .= '/>'.$newLine;
            return 
$xml;
            }

        for (
$i 0$i $numChildren; ++$i)
            {
            if (isset(
$this->value[$i]) && $i != 0)
                
$xml .= $indent.$this->value[$i].$newLine;
            
$xml .= $this->childrenNum[$i]->getXmlString$whiteSpace$indent."\t" );
            }

        if (isset(
$this->value[$numChildren]) && $numChildren != 0)
            
$xml .= $indent.$this->value[$i].$newLine;

        
$xml .= $indent.'</'.$this->name.'>'.$newLine;
        return 
$xml;
        } 
//}}}end method getXmlString


    //{{{ _parsePath (String)
    /**
     * Parse the XPath supplied in the $path argument
     *
     * @param string $path An XPath expression.
     * @return array parsed path
     * @access private
     */
    
function _parsePath$path )
        {
        if (empty(
$path))
            {
            return array(
'nextNode' => '',
                         
'index' => '',
                         
'restPath' => '');
            }
        else if (
$path{0} == '/')
            {
            
$path explode('/'$path3);
            if (
$path[1] = $this->name)
                {
                return 
$this->_parsePath($path[2]);
                }
            else
                {
                return array(
'nextNode' => '',
                             
'index' => '',
                             
'restPath' => '');
                }

            }
        else
            {
            
$path explode('/'$path2);
            }

        
$index '';

        
//If the path contains an index ([index])
        
if ($path[0]{strlen($path[0])-1} == ']')
            {
            
$start strpos($path[0], '[');
            
$length strlen($path[0]) - $start-2;
            
$index intval(substr($path[0], $start+1$length));
            
$path[0] = substr_replace($path[0], ''$start);
            }

        return array(
'nextNode' => $path[0],
                     
'index' => $index,
                     
'restPath' => empty($path[1])?'':$path[1]);
        } 
//}}}end method _parsePath

    //{{{ equals (XmlNode)
    /**
     * Check wether this and other XmlNode is the same node, not just identical.
     *
     * @param object XmlNode &$node The XmlNode which we want to check wether
     *                              is equal to this
     * @return boolean &$node === $this, as in not just the same members, but the
     *                 same object
     * @access public
     */
    
function equals( &$node )
        {
        if (!
assert ('get_class($node) == "xmlnode"'))
            {
            echo 
'Assertion failed in XmlNode::equals:<br/>';
            echo 
' Parameter $node is not of type XmlNode<br/>';
            echo 
'It\'s type is '.get_class($node).'/'.gettype($node).' ';
            echo 
'<p/>';
            
$this->_printStackTrace();
            }

        
$this->uniqID uniqid(rand(),1);
        if (isset(
$node->uniqID) && $node->uniqID == $this->uniqID)
            
$return true;
        else
            
$return false;

        unset (
$this->uniqID);
        return 
$return;
        } 
//}}}end method equals

    //{{{_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
    
}

if (!
function_exists('debug_backtrace'))
    {
    function 
debug_backtrace( )
        {
        
$arr = array('function' => __FUNCTION__,
                     
'line' => __LINE__,
                     
'file' => __FILE__,
                     
'class' => __CLASS__,
                     
'type' => 'Undefined',
                     
'args' => 'Undefined');
        return array(
$arr$arr);
        }
    }

if (!
function_exists('is_a'))
    {
    function 
is_a($obj$name)
        {
        if (
strtolower(get_class($obj)) == strtolower($name) ||
            
strtolower(get_parent_class($obj)) == strtolower($name))
            return 
true;
        else
            return 
false;
        }
    }
?>