sintonia/livesupport/modules/alib/var/mtree.php

617 lines
21 KiB
PHP

<?php
/*------------------------------------------------------------------------------
Copyright (c) 2004 Media Development Loan Fund
This file is part of the LiveSupport project.
http://livesupport.campware.org/
To report bugs, send an e-mail to bugs@campware.org
LiveSupport is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
LiveSupport is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LiveSupport; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Author : $Author: tomas $
Version : $Revision: 1.2 $
Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/alib/var/Attic/mtree.php,v $
------------------------------------------------------------------------------*/
define('ALIBERR_MTREE', 10);
/**
* Mtree class
*
* class for tree hierarchy stored in db
*
* example config: example/conf.php<br>
* example minimal config:
* <pre><code>
* $config = array(
* 'dsn' => array( // data source definition
* 'username' => DBUSER,
* 'password' => DBPASSWORD,
* 'hostspec' => 'localhost',
* 'phptype' => 'pgsql',
* 'database' => DBNAME
* ),
* 'tblNamePrefix' => 'al_',
* 'RootNode' =>'RootNode',
* );
* </code></pre>
* @author $Author: tomas $
* @version $Revision: 1.2 $
* @see ObjClasses
*/
class Mtree{
var $dbc;
var $config;
var $treeTable;
var $rootNodeName;
/** Constructor
*
* @param dbc object
* @param config array
* @return this
*/
function Mtree(&$dbc, $config)
{
$this->dbc =& $dbc;
$this->config = $config;
$this->treeTable = $config['tblNamePrefix'].'tree';
$this->rootNodeName = $config['RootNode'];
}
/* ======================================================= public methods */
/**
* Add new object of specified type to the tree under specified parent
* node as last child or after specified sibling
*
* @param name string
* @param type string
* @param parid int, optional, parent id
* @param aftid int, optional, after id
* @return int/err - new id of inserted object or PEAR::error
*/
function addObj($name, $type, $parid=1, $aftid=NULL)
{
if($name=='' || $type=='') return PEAR::raiseError(
'Mtree::addObj: Wrong name or type', ALIBERR_MTREE
);
$this->dbc->query("BEGIN");
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}");
if(PEAR::isError($r)) return $r;
// position resolving:
if(is_null($aftid)){ // add object as last child
$after = $this->dbc->getOne("
SELECT max(rgt) FROM {$this->treeTable} WHERE parid='$parid'
");
}else{ // use 'aftid'
$after = $this->dbc->getOne("
SELECT ".($aftid == $parid ? 'lft' : 'rgt')."
FROM {$this->treeTable} WHERE id='$aftid'");
}
if(PEAR::isError($after)) return $this->_dbRollback($after);
if(is_null($after)){ // position not specified - add as first child
$after = $this->dbc->getOne("
SELECT lft FROM {$this->treeTable} WHERE id='$parid'
");
}
if(PEAR::isError($after)) return $this->_dbRollback($after);
$after = intval($after);
// tree level resolving:
$level = $this->dbc->getOne("SELECT level FROM {$this->treeTable}
WHERE id='$parid'");
if(is_null($level))
return $this->_dbRollback('addObj: parent does not exist');
if(PEAR::isError($level)) return $this->_dbRollback($level);
$id = $this->dbc->nextId("{$this->treeTable}_id_seq");
if(PEAR::isError($id)) return $this->_dbRollback($id);
// creating space in rgt/lft sequencies:
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt+2
WHERE rgt>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft+2
WHERE lft>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
// inserting object:
$r = $this->dbc->query("
INSERT INTO {$this->treeTable}
(id, name, type, parid, level, lft, rgt)
VALUES
('$id', '$name', '$type', $parid,
".($level+1).", ".($after+1).", ".($after+2)."
)
");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return $id;
}
/**
* Create copy of specified object and insert copy to new position
*
* @param id int, source object id
* @param newParid int, destination parent id
* @param after int, optional, destinantion after id
* @return int/err - new id of inserted object or PEAR::error
*/
function copyObj($id, $newParid, $after=NULL)
{
$o = $this->dbc->getRow("SELECT * FROM {$this->treeTable}
WHERE id='$id'");
if(PEAR::isError($o)) return $o;
$nid = $this->addObj(
$o['name'], $o['type'], $newParid, $after, $o['param']
);
return $nid;
}
/**
* Rename of specified object
*
* @param id int, object id to rename
* @param newName string, new name
* @return boolean/err - True or PEAR::error
*/
function renameObj($id, $newName)
{
$r = $this->dbc->query("UPDATE {$this->treeTable} SET name='$newName'
WHERE id='$id'");
if(PEAR::isError($r)) return $r;
return TRUE;
}
/**
* Remove of specified object
*
* @param id int, object id to remove
* @return boolean/err - TRUE or PEAR::error
*/
function removeObj($id)
{
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
foreach($dirarr as $k=>$snod)
{
$this->removeObj($snod['id']);
}
$this->dbc->query("BEGIN");
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}");
if(PEAR::isError($r)) return $r;
$rgt = $this->dbc->getOne("SELECT rgt FROM {$this->treeTable}
WHERE id='$id'");
if(is_null($rgt))
return $this->_dbRollback('removeObj: object not exists');
// deleting object:
$r = $this->dbc->query("DELETE FROM {$this->treeTable} WHERE id='$id'");
if(PEAR::isError($r)) return $this->_dbRollback($r);
// closing the space in rgt/lft sequencies:
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt-2
WHERE rgt>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft-2
WHERE lft>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return TRUE;
}
/* --------------------------------------------------------- info methods */
/**
* Search for child id by name
*
* @param name string, searched name
* @param parId int, optional, parent id (default is root node)
* @return int/null/err - child id (if found) or null or PEAR::error
*/
function getObjId($name, $parId=NULL)
{
if($name=='' && is_null($parId)) $name = $this->rootNodeName;
return $this->dbc->getOne(
"SELECT id FROM {$this->treeTable}
WHERE name='$name' and ".($parId ? "parid='$parId'":"parid is null")
);
}
/**
* Get one value for object (default: get name) by id
*
* @param oid int
* @param fld string, optional, requested field (default: name)
* @return string/err
*/
function getObjName($oid, $fld='name')
{
return $this->dbc->getOne("SELECT $fld FROM {$this->treeTable}
WHERE id='$oid'");
}
/**
* Get object type by id
*
* @param oid int
* @return string/err
*/
function getObjType($oid)
{
return $this->getObjName($oid, 'type');
}
/**
* Get parent id
*
* @param oid int
* @return string/err
*/
function getParent($oid)
{
return $this->getObjName($oid, 'parid');
}
/**
* Get array of nodes in object's path from root node
*
* @param id int
* @param flds string, optional
* @return array/err
*/
function getPath($id, $flds='id')
{
$this->dbc->query("BEGIN");
$a = $this->dbc->getRow("SELECT name, lft, rgt FROM {$this->treeTable}
WHERE id='$id'");
$res = $this->dbc->getAll("
SELECT $flds FROM {$this->treeTable}
WHERE lft<={$a['lft']} AND rgt>={$a['rgt']}
ORDER by lft
");
$this->dbc->query("COMMIT");
return $res;
}
/**
* Get array of childnodes
*
* @param id int
* @param flds string, optional, comma separated list of requested fields
* @param order string, optional, fieldname for order by clause
* @return array/err
*/
function getDir($id, $flds='id', $order='lft')
{
return $this->dbc->getAll("
SELECT $flds FROM {$this->treeTable}
WHERE parid='$id' ORDER BY $order
");
}
/**
* Get subtree of specified node
*
* @param id int, optional, default: root node
* @param withRoot boolean, optional, include/exclude specified node
* @return array/err
*/
function getSubTree($id=NULL, $withRoot=FALSE)
{
if(is_null($id)) $id = $this->getRootNode();
$r = array();
if($withRoot) $r[] = $re = $this->dbc->getRow(
"SELECT id, name, level FROM {$this->treeTable} WHERE id='$id'"
);
if(PEAR::isError($re)) return $re;
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
foreach($dirarr as $k=>$snod)
{
$r[] = $re = $this->dbc->getRow("SELECT id, name, level
FROM {$this->treeTable} WHERE id={$snod['id']}");
if(PEAR::isError($re)) return $re;
$r = array_merge($r, $this->getSubTree($snod['id']));
}
return $r;
}
/**
* Get id of root node
*
* @return int/err
*/
function getRootNode()
{
return $this->getObjId($this->rootNodeName);
}
/**
* Get all objects in the tree
*
* @return array/err
*/
function getAllObjects()
{
return $this->dbc->getAll(
"SELECT * FROM {$this->treeTable} ORDER BY lft"
);
}
/* ------------------------ info methods related to application structure */
/* (this part should be added/rewritten to allow defining/modifying/using
* application structure)
* (only very simple structure definition - in $config - supported now)
*/
/**
* Get child types allowed by application definition
*
* @param type string
* @return array
*/
function getAllowedChildTypes($type)
{
return $this->config['objtypes'][$type];
}
/* ==================================================== "private" methods */
/**
* Do SQL rollback and return PEAR::error
*
* @param r object/string
* @return err
*/
function _dbRollback($r)
{
$this->dbc->query("ROLLBACK");
if(PEAR::isError($r)) return $r;
elseif(is_string($r)) return PEAR::raiseError(
"ERROR: ".get_class($this).": $r", ALIBERR_MTREE, PEAR_ERROR_RETURN
);
else return PEAR::raiseError(
"ERROR: ".get_class($this).": unknown error",
ALIBERR_MTREE, PEAR_ERROR_RETURN
);
}
/**
* Move subtree to another node without removing/adding
* Little bit complicated - sorry - it probably should be simlified ... ;)
*
* @param id int
* @param newParid int
* @param after int
* @return boolean/err
*/
function _relocateSubtree($id, $newParid, $after=NULL)
{
$this->dbc->query("BEGIN");
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}");
if(PEAR::isError($r)) return $r;
// obtain values for source node:
$a1 = $this->dbc->getRow("SELECT lft, rgt, level FROM {$this->treeTable}
WHERE id='$id'");
if(is_null($a1))
return $this->_dbRollback('_relocateSubtree: object not exists');
extract($a1);
// values for destination node:
$a2 = $this->dbc->getRow("SELECT rgt, level FROM {$this->treeTable}
WHERE id='$newParid'");
if(is_null($a2))return $this->_dbRollback(
'_relocateSubtree: new parent not exists'
);
$nprgt = $a2['rgt']; $newLevel = $a2['level'];
// calculate differencies:
if(is_null($after)) $after = $nprgt-1;
$dif1 = $rgt-$lft+1;
$dif2 = $after-$lft+1;
$dif3 = $newLevel-$level+1;
// relocate the object"
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET parid='$newParid' WHERE id='$id'");
if(PEAR::isError($r)) return $this->_dbRollback($r);
if($after>$rgt){
// relocate subtree to the right:
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET rgt=rgt+$dif1 WHERE rgt>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET lft=lft+$dif1 WHERE lft>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable}
SET lft=lft+$dif2, rgt=rgt+$dif2, level=level+$dif3
WHERE lft>=$lft AND rgt <=$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET rgt=rgt-$dif1 WHERE rgt>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET lft=lft-$dif1 WHERE lft>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
}else{
// relocate subtree to the left:
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET rgt=rgt+$dif1 WHERE rgt>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET lft=lft+$dif1 WHERE lft>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable}
SET lft=lft+$dif2-$dif1, rgt=rgt+$dif2-$dif1, level=level+$dif3
WHERE lft>=$lft+$dif1 AND rgt <=$rgt+$dif1");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt-$dif1
WHERE rgt>$rgt+$dif1");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft-$dif1
WHERE lft>$rgt+$dif1");
if(PEAR::isError($r)) return $this->_dbRollback($r);
}
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return TRUE;
}
/**
* Recursive copyObj - copy of whole subtree
*
* @param id int
* @param newParid int
* @param after int
* @return array
*/
function _copySubtree($id, $newParid, $after=NULL)
{
$nid = $this->copyObj($id, $newParid, $after);
if(PEAR::isError($nid)) return $nid;
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
foreach($dirarr as $k=>$snod)
{
$r = $this->_copySubtree($snod['id'], $nid);
if(PEAR::isError($r)) return $r;
}
return TRUE;
}
/* =============================================== test and debug methods */
/**
* Human readable dump of subtree - for debug
*
* @param id int
* @param indstr string, indentation string
* @param ind string, aktual indentation
* @return string
*/
function dumpTree($id=NULL, $indstr=' ', $ind='',
$format='{name}', $withRoot=TRUE)
{
$r='';
foreach($this->getSubTree($id, $withRoot) as $o)
$r .= str_repeat($indstr, intval($o['level'])).
preg_replace(array('|\{name\}|', '|\{id\}|'),
array($o['name'], $o['id']), $format).
"\n";
return $r;
}
/**
* Delete all nodes except the root
*
*/
function deleteData()
{
$this->dbc->query("DELETE FROM {$this->treeTable}
WHERE parid is not null");
}
/**
* Insert test data to the tree
*
* @return array
*/
function testData()
{
$o[] = $rootId = $this->getRootNode();
$o[] = $p1 = $this->addObj('Publication A', 'Publication', $rootId); //1
$o[] = $p2 = $this->addObj('Publication B', 'Publication', $rootId); //2
$o[] = $i1 = $this->addObj('Issue 1', 'Issue', $p1); //3
$o[] = $i2 = $this->addObj('Issue 2', 'Issue', $p1); //4
$o[] = $s1 = $this->addObj('Section a', 'Section', $i2);
$o[] = $s2 = $this->addObj('Section b', 'Section', $i2); //6
$o[] = $s3 = $this->addObj('Section c', 'Section', $i2);
$o[] = $t1 = $this->addObj('Title', 'Title', $s2);
$o[] = $s4 = $this->addObj('Section a', 'Section', $i1);
$o[] = $s5 = $this->addObj('Section b', 'Section', $i1);
$this->tdata['tree'] = $o;
}
/**
* Make basic test
*
*/
function test()
{
$this->deleteData();
$this->testData();
$rootId = $this->getRootNode();
$this->test_correct ="RootNode\n Publication A\n Issue 1\n".
" Section a\n Section b\n Issue 2\n".
" Section a\n Section b\n".
" Title\n Section c\n Publication B\n".
"RootNode\n";
$this->test_dump = $this->dumpTree();
$this->removeObj($this->tdata['tree'][1]);
$this->removeObj($this->tdata['tree'][2]);
$this->test_dump .= $this->dumpTree();
$this->deleteData();
if($this->test_dump == $this->test_correct){
$this->test_log.="tree: OK\n"; return TRUE;
}else return PEAR::raiseError('Mtree::test:', 1, PEAR_ERROR_DIE, '%s'.
"<pre>\ncorrect:\n.{$this->test_correct}.\n".
"dump:\n.{$this->test_dump}.\n</pre>\n");
}
/**
* Create tables + initialize
*
*/
function install()
{
$this->dbc->query("CREATE TABLE {$this->treeTable} (
id int not null,
name varchar(255) not null default'',
parid int,
lft int,
rgt int,
level int,
type varchar(255) not null default'',
param varchar(255)
)");
$this->dbc->query("CREATE UNIQUE INDEX {$this->treeTable}_id_idx
ON {$this->treeTable} (id)");
$this->dbc->query("CREATE INDEX {$this->treeTable}_name_idx
ON {$this->treeTable} (name)");
$this->dbc->createSequence("{$this->treeTable}_id_seq");
$id = $this->dbc->nextId("{$this->treeTable}_id_seq");
$this->dbc->query("INSERT INTO {$this->treeTable}
(id, name, parid, level, lft, rgt, type)
VALUES
($id, '{$this->rootNodeName}', NULL, 0, 1, 2, 'RootNode')");
}
/**
* Drop tables etc.
*
*/
function uninstall()
{
$this->dbc->query("DROP TABLE {$this->treeTable}");
$this->dbc->dropSequence("{$this->treeTable}_id_seq");
}
/**
* Uninstall and install
*
*/
function reinstall()
{
$this->uninstall();
$this->install();
}
}
?>