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_WARNING, 0); define('WITH_WHITESPACE', true); define('NO_WHITESPACE', false); define('ASSOC', 'assoc'); define('NUMERIC', 'num'); /** * This class represents a XML node * * @author Tommy Gildseth ; * @author Akili AS ; * @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.
* 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.
* 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:
'; echo ' Parameter $node is not of type XmlNode
'; echo 'It\'s type is '.get_class($node).'/'.gettype($node).' '; echo '

'; $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:
'; echo ' Parameter $node is not of type XmlNode
'; echo 'It\'s type is '.get_class($node).'/'.gettype($node).' '; echo '

'; $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 > 0 || (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 .= '>'.$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.'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('/', $path, 3); if ($path[1] = $this->name) { return $this->_parsePath($path[2]); } else { return array('nextNode' => '', 'index' => '', 'restPath' => ''); } } else { $path = explode('/', $path, 2); } $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:
'; echo ' Parameter $node is not of type XmlNode
'; echo 'It\'s type is '.get_class($node).'/'.gettype($node).' '; echo '

'; $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:
'; array_shift($dbt); foreach($dbt as $eachCall) { echo $eachCall['function'].' called from '.$eachCall['file'].':'; echo $eachCall['line'].'
'; } echo '

'; }//}}}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; } } ?>