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