blob: 45ca67bfd889c852552678d1a01c810ce4d3967e [file] [log] [blame]
<?php
/**
* File containing the ezcTreeXml class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @version //autogentag//
* @filesource
* @package Tree
*/
/**
* ezcTreeXml is an implementation of a tree backend that operates on
* an XML file.
*
* Example:
* <code>
* <?php
* // Create a new tree in a new XML file
* $tree = ezcTreeXml::create(
* 'new-tree.xml',
* new ezcTreeXmlInternalDataStore()
* );
*
* // Open an existing XML file containing a tree structure
* $tree = new ezcTreeXml(
* 'existing-tree.xml',
* new ezcTreeXmlInternalDataStore()
* );
* ?>
* </code>
*
* See {@link ezcTree} for examples on how to operate on the tree.
*
* @property-read ezcTreeXmlDataStore $store
* The data store that is used for retrieving/storing data.
* @property-read string $prefix
* The prefix that is used to prefix node IDs with in the tree.
* @property string $nodeClassName
* Which class is used as tree node - this class *must* inherit
* the ezcTreeNode class.
*
* @package Tree
* @version //autogentag//
* @mainclass
*/
class ezcTreeXml extends ezcTree
{
/**
* Contains the relax-NG schema to validate the tree XML.
*/
const relaxNG = '<?xml version="1.0" encoding="UTF-8"?>
<grammar xmlns:etd="http://components.ez.no/Tree/data" ns="http://components.ez.no/Tree" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<start>
<element name="tree">
<optional>
<attribute name="prefix">
<data type="ID"/>
</attribute>
</optional>
<optional>
<attribute name="lastNodeId">
<data type="string"/>
</attribute>
</optional>
<optional>
<ref name="node"/>
</optional>
</element>
</start>
<define name="node">
<element name="node">
<attribute name="id">
<data type="ID"/>
</attribute>
<optional>
<element name="etd:data">
<text/>
</element>
</optional>
<zeroOrMore>
<ref name="node"/>
</zeroOrMore>
</element>
</define>
</grammar>';
/**
* Contains the DOM Tree that all operations will be done on.
*
* When the tree object is constructed the XML is parsed with DOM and
* stored into this member variable. When the tree is modified the changes
* are then flushed to disk with the saveFile() method.
*
* @var DOMDocument $dom
*/
private $dom;
/**
* The file name that contains the tree as XML string.
*
* @var string $xmlFile
*/
private $xmlFile;
/**
* Stores the last auto generated ID that was used.
*
* @var integer $autoNodeId
*/
private $autoNodeId = 0;
/**
* Constructs a new ezcTreeXml object from the XML data in $xmlFile and using
* the $store to retrieve data from.
*
* @param string $xmlFile
* @param ezcTreeXmlDataStore $store
*/
public function __construct( $xmlFile, ezcTreeXmlDataStore $store )
{
if ( !file_exists( $xmlFile ) )
{
throw new ezcBaseFileNotFoundException( $xmlFile, "XML" );
}
$previous = libxml_use_internal_errors( true );
$dom = new DomDocument();
$dom->load( $xmlFile );
$dom->formatOutput = true;
$errors = libxml_get_errors();
libxml_clear_errors();
if ( count( $errors ) )
{
throw new ezcTreeInvalidXmlException( $xmlFile, $errors );
}
$valid = $dom->relaxNGValidateSource( self::relaxNG );
if ( !$valid )
{
$errors = libxml_get_errors();
libxml_clear_errors();
throw new ezcTreeInvalidXmlFormatException( $xmlFile, $errors );
}
libxml_use_internal_errors( $previous );
// Associate the DOM tree with the data store
$store->setDomTree( $dom );
// Figure out the prefix - which is the "prefix" attribute on the root node.
$document = $dom->documentElement;
$prefix = $document->getAttribute( 'prefix' );
// Figure out the last auto generated ID
$autoId = false;
$this->autoNodeId = $document->getAttribute( 'lastNodeId' );
if ( $this->autoNodeId !== "" )
{
$autoId = true;
}
// Set member variables
$this->dom = $dom;
$this->xmlFile = $xmlFile;
$this->properties['store'] = $store;
$this->properties['prefix'] = $prefix;
$this->properties['autoId'] = $autoId;
}
/**
* Returns the value of the property $name.
*
* @throws ezcBasePropertyNotFoundException if the property does not exist.
* @param string $name
* @ignore
*/
public function __get( $name )
{
switch ( $name )
{
case 'prefix':
return $this->properties[$name];
}
return parent::__get( $name );
}
/**
* Sets the property $name to $value.
*
* @throws ezcBasePropertyNotFoundException if the property does not exist.
* @throws ezcBasePropertyPermissionException if a read-only property is
* tried to be modified.
* @param string $name
* @param mixed $value
* @ignore
*/
public function __set( $name, $value )
{
switch ( $name )
{
case 'prefix':
throw new ezcBasePropertyPermissionException( $name, ezcBasePropertyPermissionException::READ );
default:
return parent::__set( $name, $value );
}
}
/**
* Returns true if the property $name is set, otherwise false.
*
* @param string $name
* @return bool
* @ignore
*/
public function __isset( $name )
{
switch ( $name )
{
case 'prefix':
return isset( $this->properties[$name] );
default:
return parent::__isset( $name );
}
}
/**
* Creates a new XML tree in the file $xmlFile using $store as data store.
*
* The $prefix option can be used to change the prefix that is used for IDs
* in the created tree. By default this is set to "id" so that numerical
* node IDs are not a problem.
*
* @param string $xmlFile
* @param ezcTreeXmlDataStore $store
* @param string $prefix
* @return ezcTreeXml
*/
public static function create( $xmlFile, ezcTreeXmlDataStore $store, $prefix = 'id' )
{
$dom = new DomDocument( '1.0', 'utf-8' );
$dom->formatOutput = true;
$element = $dom->createElement( 'tree' );
$element->setAttributeNode( new DOMAttr( 'xmlns', 'http://components.ez.no/Tree' ) );
$element->setAttributeNode( new DOMAttr( 'xmlns:etd', 'http://components.ez.no/Tree/data' ) );
if ( $prefix !== null )
{
$element->setAttributeNode( new DOMAttr( 'prefix', $prefix ) );
}
$dom->appendChild( $element );
$dom->save( $xmlFile );
return new ezcTreeXml( $xmlFile, $store, $prefix );
}
/**
* Saves the internal DOM representation of the tree back to disk.
*/
public function saveFile()
{
$this->dom->save( $this->xmlFile );
}
/**
* This method generates the next node ID.
*
* It uses the stored last generated ID, and also stores this in an
* attribute on the root node so that it can be correctly incremented
* without having to search for the last generated ID in the whole tree.
*
* @return integer
*/
protected function generateNodeID()
{
$this->autoNodeId++;
$document = $this->dom->documentElement;
$document->setAttributeNode( new DOMAttr( 'lastNodeId', $this->autoNodeId ) );
return $this->autoNodeId;
}
/**
* Returns whether the node with ID $nodeId exists.
*
* @param string $nodeId
* @return bool
*/
public function nodeExists( $nodeId )
{
$elem = $this->dom->getElementById( "{$this->properties['prefix']}$nodeId" );
return ( $elem !== null ) ? true : false;
}
/**
* Retrieves a DOMElement containing the node with node ID $nodeId.
*
* @param string $nodeId
* @return DOMElement
*/
private function getNodeById( $nodeId )
{
$node = $this->dom->getElementById( "{$this->properties['prefix']}$nodeId" );
if ( !$node )
{
throw new ezcTreeUnknownIdException( $nodeId );
}
return $node;
}
/**
* Fetches all the child "node" DOM elements of the node with ID $nodeId.
*
* @param string $nodeId
* @return array(string)
*/
private function fetchChildIds( $nodeId )
{
$childNodes = array();
$elem = $this->getNodeById( $nodeId );
$children = $elem->childNodes;
foreach ( $children as $child )
{
if ( $child->nodeType === XML_ELEMENT_NODE && $child->tagName == "node" )
{
$nodeId = substr( $child->getAttribute( 'id' ), strlen( $this->properties['prefix'] ) );
$childNodes[] = $nodeId;
}
}
return $childNodes;
}
/**
* Returns all the children of the node with ID $nodeId.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
public function fetchChildren( $nodeId )
{
$className = $this->properties['nodeClassName'];
$list = new ezcTreeNodeList;
foreach ( $this->fetchChildIds( $nodeId ) as $childId )
{
$list->addNode( new $className( $this, $childId ) );
}
return $list;
}
/**
* Returns the parent node of the node with ID $nodeId.
*
* This method returns null if there is no parent node.
*
* @param string $nodeId
* @return ezcTreeNode
*/
public function fetchParent( $nodeId )
{
$className = $this->properties['nodeClassName'];
$elem = $this->getNodeById( $nodeId );
$elem = $elem->parentNode;
$parentId = $elem !== null ? substr( $elem->getAttribute( 'id' ), strlen( $this->properties['prefix'] ) ) : null;
if ( $parentId === false )
{
return null;
}
return new $className( $this, $parentId );
}
/**
* Returns all the nodes in the path from the root node to the node with ID
* $nodeId, including those two nodes.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
public function fetchPath( $nodeId )
{
$className = $this->properties['nodeClassName'];
$nodes = array();
$nodes[] = new $className( $this, $nodeId );
$elem = $this->getNodeById( $nodeId );
$elem = $elem->parentNode;
while ( $elem !== null && $elem->nodeType == XML_ELEMENT_NODE && $elem->tagName == 'node' )
{
$id = substr( $elem->getAttribute( 'id' ), strlen( $this->properties['prefix'] ) );
$nodes[] = new $className( $this, $id );
$elem = $elem->parentNode;
}
$list = new ezcTreeNodeList;
foreach ( array_reverse( $nodes ) as $node )
{
$list->addNode( $node );
}
return $list;
}
/**
* Adds the children nodes of the node with ID $nodeId to the
* ezcTreeNodeList $list.
*
* @param ezcTreeNodeList $list
* @param string $nodeId
*/
private function addChildNodesDepthFirst( ezcTreeNodeList $list, $nodeId )
{
$className = $this->properties['nodeClassName'];
foreach ( $this->fetchChildIds( $nodeId ) as $childId )
{
$list->addNode( new $className( $this, $childId ) );
$this->addChildNodesDepthFirst( $list, $childId );
}
}
/**
* Returns the node with ID $nodeId and all its children, sorted according to
* the {@link http://en.wikipedia.org/wiki/Depth-first_search Depth-first sorting}
* algorithm.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
public function fetchSubtreeDepthFirst( $nodeId )
{
$className = $this->properties['nodeClassName'];
$list = new ezcTreeNodeList;
$list->addNode( new $className( $this, $nodeId ) );
$this->addChildNodesDepthFirst( $list, $nodeId );
return $list;
}
/**
* Alias for fetchSubtreeDepthFirst().
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
public function fetchSubtree( $nodeId )
{
return $this->fetchSubtreeDepthFirst( $nodeId );
}
/**
* Adds the children nodes of the node with ID $nodeId to the
* ezcTreeNodeList $list.
*
* @param ezcTreeNodeList $list
* @param string $nodeId
*/
private function addChildNodesBreadthFirst( ezcTreeNodeList $list, $nodeId )
{
$className = $this->properties['nodeClassName'];
$childIds = $this->fetchChildIds( $nodeId );
foreach ( $childIds as $childId )
{
$list->addNode( new $className( $this, $childId ) );
}
foreach ( $childIds as $childId )
{
$this->addChildNodesDepthFirst( $list, $childId );
}
}
/**
* Returns the node with ID $nodeId and all its children, sorted according to
* the {@link http://en.wikipedia.org/wiki/Breadth-first_search Breadth-first sorting}
* algorithm.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
public function fetchSubtreeBreadthFirst( $nodeId )
{
$className = $this->properties['nodeClassName'];
$list = new ezcTreeNodeList;
$list->addNode( new $className( $this, $nodeId ) );
$this->addChildNodesBreadthFirst( $list, $nodeId );
return $list;
}
/**
* Returns the number of direct children of the node with ID $nodeId.
*
* @param string $nodeId
* @return int
*/
public function getChildCount( $nodeId )
{
$count = 0;
$elem = $this->getNodeById( $nodeId );
$children = $elem->childNodes;
foreach ( $children as $child )
{
if ( $child->nodeType === XML_ELEMENT_NODE && $child->tagName == "node" )
{
$count++;
}
}
return $count;
}
/**
* Adds the number of children with for the node with ID $nodeId nodes to
* $count, recursively.
*
* @param int $count
* @param string $nodeId
*/
private function countChildNodes( &$count, $nodeId )
{
foreach ( $this->fetchChildIds( $nodeId ) as $childId )
{
$count++;
$this->countChildNodes( $count, $childId );
}
}
/**
* Returns the number of children of the node with ID $nodeId, recursively.
*
* @param string $nodeId
* @return int
*/
public function getChildCountRecursive( $nodeId )
{
$count = 0;
$this->countChildNodes( $count, $nodeId );
return $count;
}
/**
* Returns the distance from the root node to the node with ID $nodeId.
*
* @param string $nodeId
* @return int
*/
public function getPathLength( $nodeId )
{
$elem = $this->getNodeById( $nodeId );
$elem = $elem->parentNode;
$length = -1;
while ( $elem !== null && $elem->nodeType == XML_ELEMENT_NODE )
{
$elem = $elem->parentNode;
$length++;
}
return $length;
}
/**
* Returns whether the node with ID $nodeId has children.
*
* @param string $nodeId
* @return bool
*/
public function hasChildNodes( $nodeId )
{
$elem = $this->getNodeById( $nodeId );
$children = $elem->childNodes;
foreach ( $children as $child )
{
if ( $child->nodeType === XML_ELEMENT_NODE && $child->tagName == "node" )
{
return true;
}
}
return false;
}
/**
* Returns whether the node with ID $childId is a direct child of the node
* with ID $parentId.
*
* @param string $childId
* @param string $parentId
* @return bool
*/
public function isChildOf( $childId, $parentId )
{
$elem = $this->getNodeById( $childId );
$parentElem = $elem->parentNode;
$nodeId = $parentElem->getAttribute( 'id' );
if ( $nodeId === "{$this->properties['prefix']}$parentId" )
{
return true;
}
return false;
}
/**
* Returns whether the node with ID $childId is a direct or indirect child
* of the node with ID $parentId.
*
* @param string $childId
* @param string $parentId
* @return bool
*/
public function isDescendantOf( $childId, $parentId )
{
$elem = $this->getNodeById( $childId );
$elem = $elem->parentNode;
while ( $elem !== null && $elem->nodeType == XML_ELEMENT_NODE )
{
$nodeId = $elem->getAttribute( 'id' );
if ( $nodeId === "{$this->properties['prefix']}$parentId" )
{
return true;
}
$elem = $elem->parentNode;
}
return false;
}
/**
* Returns whether the nodes with IDs $child1Id and $child2Id are siblings
* (ie, they share the same parent).
*
* @param string $child1Id
* @param string $child2Id
* @return bool
*/
public function isSiblingOf( $child1Id, $child2Id )
{
$elem1 = $this->getNodeById( $child1Id );
$elem2 = $this->getNodeById( $child2Id );
return (
( $child1Id !== $child2Id ) &&
( $elem1->parentNode->getAttribute( 'id' ) === $elem2->parentNode->getAttribute( 'id' ) )
);
}
/**
* Sets a new node as root node, this also wipes out the whole tree.
*
* @param ezcTreeNode $node
*/
public function setRootNode( ezcTreeNode $node )
{
$document = $this->dom->documentElement;
// remove old root node(s)
foreach ( $document->childNodes as $childNode )
{
if ( $childNode->nodeType == XML_ELEMENT_NODE && $childNode->tagName === 'node' )
{
$nodeId = substr( $childNode->getAttribute( 'id' ), strlen( $this->properties['prefix'] ) );
$this->delete( $nodeId );
}
}
$this->store->deleteDataForAllNodes();
// Create new root node
$root = $this->dom->createElement( 'node' );
$root->setAttributeNode( new DOMAttr( 'id', "{$this->properties['prefix']}{$node->id}" ) );
$root->setIdAttribute( 'id', true );
$document->appendChild( $root );
$this->store->storeDataForNode( $node, $node->data );
$this->saveFile();
}
/**
* Returns the root node.
*
* This methods returns null if there is no root node.
*
* @return ezcTreeNode
*/
public function getRootNode()
{
$className = $this->properties['nodeClassName'];
$document = $this->dom->documentElement;
foreach ( $document->childNodes as $childNode )
{
if ( $childNode->nodeType == XML_ELEMENT_NODE && $childNode->tagName == 'node' )
{
$nodeId = substr( $childNode->getAttribute( 'id' ), strlen( $this->properties['prefix'] ) );
return new $className( $this, $nodeId );
}
}
return null;
}
/**
* Adds the node $childNode as child of the node with ID $parentId.
*
* @param string $parentId
* @param ezcTreeNode $childNode
*/
public function addChild( $parentId, ezcTreeNode $childNode )
{
if ( $this->inTransaction )
{
$this->addTransactionItem( new ezcTreeTransactionItem( ezcTreeTransactionItem::ADD, $childNode, null, $parentId ) );
return;
}
// locate parent node
$elem = $this->getNodeById( $parentId );
// Create new DOM node
$child = $this->dom->createElement( 'node' );
$child->setAttributeNode( new DOMAttr( 'id', "{$this->properties['prefix']}{$childNode->id}" ) );
$child->setIdAttribute( 'id', true );
// Append to parent node
$elem->appendChild( $child );
$this->store->storeDataForNode( $childNode, $childNode->data );
if ( !$this->inTransactionCommit )
{
$this->saveFile();
}
}
/**
* Deletes the node with ID $nodeId from the tree, including all its children.
*
* @param string $nodeId
*/
public function delete( $nodeId )
{
if ( $this->inTransaction )
{
$this->addTransactionItem( new ezcTreeTransactionItem( ezcTreeTransactionItem::DELETE, null, $nodeId ) );
return;
}
// Delete all the associated data
$nodeList = $this->fetchSubtree( $nodeId );
$this->store->deleteDataForNodes( $nodeList );
// locate node to move
$nodeToDelete = $this->getNodeById( $nodeId );
// Remove the ID on all children by hand as this would crash in PHP <= 5.2.3
$nodeToDelete->removeAttribute( "id" );
$children = $nodeToDelete->getElementsByTagName( 'node' );
foreach ( $children as $child )
{
$child->removeAttribute( "id" );
}
// Use the parent to remove the child
$nodeToDelete->parentNode->removeChild( $nodeToDelete );
if ( !$this->inTransactionCommit )
{
$this->saveFile();
}
}
/**
* Moves the node with ID $nodeId as child to the node with ID $targetParentId.
*
* @param string $nodeId
* @param string $targetParentId
*/
public function move( $nodeId, $targetParentId )
{
if ( $this->inTransaction )
{
$this->addTransactionItem( new ezcTreeTransactionItem( ezcTreeTransactionItem::MOVE, null, $nodeId, $targetParentId ) );
return;
}
// locate node to move
$nodeToMove = $this->getNodeById( $nodeId );
// locate new parent
$parent = $this->getNodeById( $targetParentId );
$parent->appendChild( $nodeToMove );
if ( !$this->inTransactionCommit )
{
$this->saveFile();
}
}
/**
* Fixates the transaction (saves the XML file).
*/
public function fixateTransaction()
{
$this->saveFile();
}
}
?>