ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
class.ilMaterializedPathTree.php
Go to the documentation of this file.
1<?php
2/* Copyright (c) 1998-2009 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4include_once './Services/Tree/interfaces/interface.ilTreeImplementation.php';
5
17{
19 private $tree = null;
20
25 public function __construct(ilTree $a_tree)
26 {
27 $this->tree = $a_tree;
28 }
29
30
35 protected function getMaximumPossibleDepth()
36 {
38 }
39
44 public function getTree()
45 {
46 return $this->tree;
47 }
48
54 public function getSubTreeIds($a_node_id)
55 {
56 global $ilDB;
57
58 $node = $this->getTree()->getNodeTreeData($a_node_id);
59 $query = 'SELECT child FROM ' . $this->getTree()->getTreeTable() . ' ' .
60 'WHERE path BETWEEN ' .
61 $ilDB->quote($node['path'], 'text') . ' AND ' .
62 $ilDB->quote($node['path'] . '.Z', 'text') . ' ' .
63 'AND child != %s ' .
64 'AND ' . $this->getTree()->getTreePk() . ' = %s';
65
66 $res = $ilDB->queryF(
67 $query,
68 array('integer', 'integer'),
69 array($a_node_id, $this->getTree()->getTreeId())
70 );
71 while ($row = $ilDB->fetchAssoc($res)) {
72 $childs[] = $row['child'];
73 }
74 return $childs ? $childs : array();
75 }
76
83 public function getRelation($a_node_a, $a_node_b)
84 {
85 if ($a_node_a['child'] == $a_node_b['child']) {
86 ilLoggerFactory::getLogger('tree')->debug('EQUALS');
88 }
89 if (stripos($a_node_a['path'], $a_node_b['path'] . '.') === 0) {
90 ilLoggerFactory::getLogger('tree')->debug('CHILD');
92 }
93 if (stripos($a_node_b['path'], $a_node_a['path'] . '.') === 0) {
94 ilLoggerFactory::getLogger('tree')->debug('PARENT');
96 }
97 $path_a = substr($a_node_a['path'], 0, strrpos($a_node_a['path'], '.'));
98 $path_b = substr($a_node_b['path'], 0, strrpos($a_node_b['path'], '.'));
99
100 ilLoggerFactory::getLogger('tree')->debug('Comparing ' . $path_a . ' ' . 'with ' . $path_b);
101
102 if ($a_node_a['path'] and (strcmp($path_a, $path_b) === 0)) {
103 ilLoggerFactory::getLogger('tree')->debug('SIBLING');
105 }
106
107 ilLoggerFactory::getLogger('tree')->debug('NONE');
109 }
110
120 public function getSubTreeQuery($a_node, $a_types = '', $a_force_join_reference = true, $a_fields = array())
121 {
122 global $ilDB;
123
124 $type_str = '';
125 if (is_array($a_types)) {
126 if ($a_types) {
127 $type_str = "AND " . $ilDB->in($this->getTree()->getObjectDataTable() . ".type", $a_types, false, "text");
128 }
129 } elseif (strlen($a_types)) {
130 $type_str = "AND " . $this->getTree()->getObjectDataTable() . ".type = " . $ilDB->quote($a_types, "text");
131 }
132
133 $join = '';
134 if ($type_str or $a_force_join_reference) {
135 $join = $this->getTree()->buildJoin();
136 }
137
138 $fields = '* ';
139 if (count($a_fields)) {
140 $fields = implode(',', $a_fields);
141 }
142
143 // @todo order by
144 $query = 'SELECT ' .
145 $fields . ' ' .
146 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
147 $join . ' ' .
148 'WHERE ' . $this->getTree()->getTreeTable() . '.path ' .
149 'BETWEEN ' .
150 $ilDB->quote($a_node['path'], 'text') . ' AND ' .
151 $ilDB->quote($a_node['path'] . '.Z', 'text') . ' ' .
152 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer') . ' ' .
153 $type_str . ' ' .
154 'ORDER BY ' . $this->getTree()->getTreeTable() . '.path';
155
156 return $query;
157 }
158
165 public function getPathIds($a_endnode, $a_startnode = 0)
166 {
167 global $ilDB;
168
169 $ilDB->setLimit(1);
170 $query = 'SELECT path FROM ' . $this->getTree()->getTreeTable() . ' ' .
171 'WHERE child = ' . $ilDB->quote($a_endnode, 'integer') . ' ';
172 $res = $ilDB->query($query);
173
174 $path = null;
175 while ($row = $ilDB->fetchAssoc($res)) {
176 $path = $row['path'];
177 }
178
179 $pathIds = explode('.', $path);
180
181 if ($a_startnode != 0) {
182 while (count($pathIds) > 0 && $pathIds[0] != $a_startnode) {
183 array_shift($pathIds);
184 }
185 }
186 return $pathIds;
187 }
188
197 public function insertNode($a_node_id, $a_parent_id, $a_pos)
198 {
199 global $ilDB;
200
201 $insert_node_callable = function (ilDBInterface $ilDB) use ($a_node_id, $a_parent_id, $a_pos) {
202 // get path and depth of parent
203 $ilDB->setLimit(1);
204
205 $res = $ilDB->queryF(
206 'SELECT parent, depth, path FROM ' . $this->getTree()->getTreeTable() . ' ' .
207 'WHERE child = %s ' . ' ' .
208 'AND ' . $this->getTree()->getTreePk() . ' = %s',
209 array('integer', 'integer'),
210 array($a_parent_id, $this->getTree()->getTreeId())
211 );
212
213
214 $r = $ilDB->fetchObject($res);
215
216 if ($r->parent === null) {
218 throw new ilInvalidTreeStructureException('Parent node not found in tree');
219 }
220
221 if ($r->depth >= $this->getMaximumPossibleDepth()) {
223 throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
224 }
225
226 $parentPath = $r->path;
227 $depth = $r->depth + 1;
228 $lft = 0;
229 $rgt = 0;
230
231
232 $ilDB->insert($this->getTree()->getTreeTable(), array($this->getTree()->getTreePk() => array('integer', $this->getTree()->getTreeId()),
233 'child' => array('integer', $a_node_id),
234 'parent' => array('integer', $a_parent_id),
235 'lft' => array('integer', $lft),
236 'rgt' => array('integer', $rgt),
237 'depth' => array('integer', $depth),
238 'path' => array('text', $parentPath . "." . $a_node_id)));
239 };
240
241 // use ilAtomQuery to lock tables if tree is main tree
242 // otherwise just call this closure without locking
243 if ($this-> getTree()->__isMainTree()) {
244 $ilAtomQuery = $ilDB->buildAtomQuery();
245 $ilAtomQuery->addTableLock("tree");
246
247 $ilAtomQuery->addQueryCallable($insert_node_callable);
248
249 $ilAtomQuery->run();
250 } else {
251 $insert_node_callable($ilDB);
252 }
253 }
254
255
262 public function deleteTree($a_node_id)
263 {
264 global $ilDB;
265 $delete_tree_callable = function (ilDBInterface $ilDB) use ($a_node_id) {
266 $query = 'SELECT * FROM ' . $this->getTree()->getTreeTable() . ' ' .
267 'WHERE ' . $this->getTree()->getTreeTable() . '.child = %s ' .
268 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' = %s ';
269 $res = $ilDB->queryF($query, array('integer','integer'), array(
270 $a_node_id,
271 $this->getTree()->getTreeId()));
272 $row = $ilDB->fetchAssoc($res);
273
274 $query = 'DELETE FROM ' . $this->getTree()->getTreeTable() . ' ' .
275 'WHERE path BETWEEN ' . $ilDB->quote($row['path'], 'text') . ' ' .
276 'AND ' . $ilDB->quote($row['path'] . '.Z', 'text') . ' ' .
277 'AND ' . $this->getTree()->getTreePk() . ' = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer');
278 $ilDB->manipulate($query);
279 };
280
281 // get lft and rgt values. Don't trust parameter lft/rgt values of $a_node
282 if ($this->getTree()->__isMainTree()) {
283 $ilAtomQuery = $ilDB->buildAtomQuery();
284 $ilAtomQuery->addTableLock('tree');
285 $ilAtomQuery->addQueryCallable($delete_tree_callable);
286 $ilAtomQuery->run();
287 } else {
288 $delete_tree_callable($ilDB);
289 }
290
291 return true;
292 }
293
300 public function moveToTrash($a_node_id)
301 {
302 global $ilDB;
303
304 $move_to_trash_callable = function (ilDBInterface $ilDB) use ($a_node_id) {
305 $node = $this->getTree()->getNodeTreeData($a_node_id);
306
307 // Set the nodes deleted (negative tree id)
308 $ilDB->manipulateF(
309 '
310 UPDATE ' . $this->getTree()->getTreeTable() . ' ' .
311 'SET tree = %s' . ' ' .
312 'WHERE ' . $this->getTree()->getTreePk() . ' = %s ' .
313 'AND path BETWEEN %s AND %s',
314 array('integer', 'integer', 'text', 'text'),
315 array(-$a_node_id, $this->getTree()->getTreeId(), $node['path'], $node['path'] . '.Z')
316 );
317 };
318
319 // use ilAtomQuery to lock tables if tree is main tree
320 // otherwise just call this closure without locking
321 if ($this->getTree()->__isMainTree()) {
322 $ilAtomQuery = $ilDB->buildAtomQuery();
323 $ilAtomQuery->addTableLock("tree");
324
325 $ilAtomQuery->addQueryCallable($move_to_trash_callable);
326
327 $ilAtomQuery->run();
328 } else {
329 $move_to_trash_callable($ilDB);
330 }
331
332 return true;
333 }
334
344 public function moveTree($a_source_id, $a_target_id, $a_position)
345 {
346 global $ilDB;
347
348 $move_tree_callable = function (ilDBInterface $ilDB) use ($a_source_id, $a_target_id, $a_position) {
349 // Receive node infos for source and target
350 $ilDB->setLimit(2);
351
352 $res = $ilDB->query(
353 'SELECT depth, child, parent, path FROM ' . $this->getTree()->getTreeTable() . ' ' .
354 'WHERE ' . $ilDB->in('child', array($a_source_id, $a_target_id), false, 'integer') . ' ' .
355 'AND tree = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer')
356 );
357
358 // Check in tree
359 if ($ilDB->numRows($res) != 2) {
360 ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
361 throw new InvalidArgumentException('Error moving subtree');
362 }
363
364 while ($row = $ilDB->fetchObject($res)) {
365 if ($row->child == $a_source_id) {
366 $source_path = $row->path;
367 $source_depth = $row->depth;
368 $source_parent = $row->parent;
369 } else {
370 $target_path = $row->path;
371 $target_depth = $row->depth;
372 }
373 }
374
375 if ($target_depth >= $source_depth) {
376 // We move nodes deeper into the tree. Therefore we need to
377 // check whether we might exceed the maximal path length.
378 // We use FOR UPDATE here, because we don't want anyone to
379 // insert new nodes while we move the subtree.
380
381 $res = $ilDB->queryF(
382 'SELECT MAX(depth) max_depth ' .
383 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
384 'WHERE path BETWEEN %s AND %s' . ' ' .
385 'AND tree = %s ',
386 array('text', 'text', 'integer'),
387 array($source_path, $source_path . '.Z', $this->getTree()->getTreeId())
388 );
389
390 $row = $ilDB->fetchObject($res);
391
392 if ($row->max_depth - $source_depth + $target_depth + 1 > $this->getMaximumPossibleDepth()) {
393 ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
394 throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
395 }
396 }
397 // Check target not child of source
398 if (substr($target_path . '.', 0, strlen($source_path) . '.') == $source_path . '.') {
399 ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Target is child of source');
400 throw new InvalidArgumentException('Error moving subtree: target is child of source');
401 }
402 $depth_diff = $target_depth - $source_depth + 1;
403
404 // move subtree:
405 $query = '
406 UPDATE ' . $this->getTree()->getTreeTable() . '
407 SET parent = CASE WHEN parent = ' . $ilDB->quote($source_parent, 'integer') . '
408 THEN ' . $ilDB->quote($a_target_id, 'integer') . '
409 ELSE parent END,
410
411 path = ' . $ilDB->concat(array(
412 array($ilDB->quote($target_path, 'text'), 'text'),
413 array($ilDB->substr('path', strrpos('.' . $source_path, '.')), 'text'))) . ' ,
414
415 depth = depth + ' . $ilDB->quote($depth_diff, 'integer') . '
416
417 WHERE path BETWEEN ' . $ilDB->quote($source_path, 'text') . '
418 AND ' . $ilDB->quote($source_path . '.Z', 'text') . '
419
420 AND tree = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer');
421
422 ilLoggerFactory::getLogger('tree')->debug('Query is ' . $query);
423
424 $ilDB->manipulate($query);
425 };
426
427
428 if ($this->getTree()->__isMainTree()) {
429 $ilAtomQuery = $ilDB->buildAtomQuery();
430 $ilAtomQuery->addTableLock("tree");
431 $ilAtomQuery->addQueryCallable($move_tree_callable);
432 $ilAtomQuery->run();
433 } else {
434 $move_tree_callable($ilDB);
435 }
436
437 return true;
438 }
439
440
441 public static function createFromParentReleation()
442 {
443 global $ilDB;
444
445 $r = $ilDB->queryF('SELECT DISTINCT * FROM tree WHERE parent = %s', array('integer'), array(0));
446
447 while ($row = $ilDB->fetchAssoc($r)) {
449
450 if ($success !== true) {
451 }
452 }
453 }
454
460 private static function createMaterializedPath($parent, $parentPath)
461 {
462 global $ilDB;
463 $q = ' UPDATE tree
464 SET path = CONCAT(COALESCE(' . $ilDB->quote($parentPath, 'text') . ', \'\'), COALESCE( ' . $ilDB->cast("child", "text") . ' , \'\'))
465 WHERE parent = %s';
466 $r = $ilDB->manipulateF($q, array('integer'), array($parent));
467
468 $r = $ilDB->queryF('SELECT child FROM tree WHERE parent = %s', array('integer'), array($parent));
469
470 while ($row = $ilDB->fetchAssoc($r)) {
471 self::createMaterializedPath($row['child'], $parentPath . $row['child'] . '.');
472 }
473
474 return true;
475 }
476
481 public function getSubtreeInfo($a_endnode_id)
482 {
483 global $ilDB;
484
485 // This is an optimization without the temporary tables become too big for our system.
486 // The idea is to use a subquery to join and filter the trees, and only the result
487 // is joined to obj_reference and obj_data.
488
489 $query = "SELECT t2.child child, type, t2.path path " .
490 "FROM " . $this->getTree()->getTreeTable() . " t1 " .
491 "JOIN " . $this->getTree()->getTreeTable() . " t2 ON (t2.path BETWEEN t1.path AND CONCAT(t1.path, '.Z')) " .
492 "JOIN " . $this->getTree()->getTableReference() . " obr ON t2.child = obr.ref_id " .
493 "JOIN " . $this->getTree()->getObjectDataTable() . " obd ON obr.obj_id = obd.obj_id " .
494 "WHERE t1.child = " . $ilDB->quote($a_endnode_id, 'integer') . " " .
495 "AND t1." . $this->getTree()->getTreePk() . " = " . $ilDB->quote($this->getTree()->getTreeId(), 'integer') . " " .
496 "AND t2." . $this->getTree()->getTreePk() . " = " . $ilDB->quote($this->getTree()->getTreeId(), 'integer') . " " .
497 "ORDER BY t2.path";
498
499
500 $res = $ilDB->query($query);
501 $nodes = array();
502 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
503 #$nodes[$row->child]['lft'] = $row->lft;
504 #$nodes[$row->child]['rgt'] = $row->rgt;
505 $nodes[$row->child]['child'] = $row->child;
506 $nodes[$row->child]['type'] = $row->type;
507 $nodes[$row->child]['path'] = $row->path;
508 }
509
510 $depth_first_compare = function ($a, $b) {
511 $a_exploded = explode('.', $a['path']);
512 #ilLoggerFactory::getLogger('tree')->debug(print_r($a_exploded,TRUE));
513 $b_exploded = explode('.', $b['path']);
514
515 $a_padded = '';
516 foreach ($a_exploded as $num) {
517 $a_padded .= (str_pad((string) $num, 14, '0', STR_PAD_LEFT));
518 }
519 $b_padded = '';
520 foreach ($b_exploded as $num) {
521 $b_padded .= (str_pad((string) $num, 14, '0', STR_PAD_LEFT));
522 }
523
524 #ilLoggerFactory::getLogger('tree')->debug($a_padded);
525 return strcasecmp($a_padded, $b_padded);
526 };
527
528 #ilLoggerFactory::getLogger('tree')->debug(print_r($nodes,TRUE));
529
530 uasort($nodes, $depth_first_compare);
531
532 #ilLoggerFactory::getLogger('tree')->debug(print_r($nodes,TRUE));
533
534 return (array) $nodes;
535 }
536
541 public function validateParentRelations()
542 {
543 global $ilDB;
544
545 $query = 'select child from ' . $this->getTree()->getTreeTable() . ' child where not exists ' .
546 '( ' .
547 'select child from ' . $this->getTree()->getTreeTable() . ' parent where child.parent = parent.child and ' .
548 '(child.path BETWEEN parent.path AND CONCAT(parent.path,' . $ilDB->quote('Z', 'text') . ') )' . ')' .
549 'and ' . $this->getTree()->getTreePk() . ' = ' . $this->getTree()->getTreeId() . ' and child <> 1';
550 $res = $ilDB->query($query);
551
552 ilLoggerFactory::getLogger('tree')->debug($query);
553
554 $failures = array();
555 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
556 $failures[] = $row[$this->getTree()->getTreePk()];
557 }
558 return $failures;
559 }
560}
$success
Definition: Utf8Test.php:86
An exception for terminatinating execution or to throw for unit testing.
Thrown if invalid tree strucutes are found.
static getLogger($a_component_id)
Get component logger.
Base class for materialize path based trees Based on implementation of Werner Randelshofer.
moveTree($a_source_id, $a_target_id, $a_position)
move source subtree to target node
getPathIds($a_endnode, $a_startnode=0)
Get path ids.
validateParentRelations()
Validaate parent relations.
getSubTreeIds($a_node_id)
Get subtree ids.
deleteTree($a_node_id)
Delete a subtree.
getSubTreeQuery($a_node, $a_types='', $a_force_join_reference=true, $a_fields=array())
Get subtree query.
__construct(ilTree $a_tree)
Constructor.
getMaximumPossibleDepth()
Get maximum possible depth.
moveToTrash($a_node_id)
Move subtree to trash.
getRelation($a_node_a, $a_node_b)
Get relation of two nodes.
insertNode($a_node_id, $a_parent_id, $a_pos)
Insert new node under parent node.
static createMaterializedPath($parent, $parentPath)
Tree class data representation in hierachical trees using the Nested Set Model with Gaps by Joe Celco...
const RELATION_EQUALS
const RELATION_PARENT
const RELATION_NONE
const RELATION_SIBLING
const RELATION_CHILD
$r
Definition: example_031.php:79
Interface ilDBInterface.
Interface for tree implementations Currrently nested set or materialize path.
$query
foreach($_POST as $key=> $value) $res
global $ilDB