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 
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 $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');
108  return ilTree::RELATION_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)) {
448  $success = self::createMaterializedPath(0, '');
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 }
Thrown if invalid tree strucutes are found.
getMaximumPossibleDepth()
Get maximum possible depth.
deleteTree($a_node_id)
Delete a subtree.
static createMaterializedPath($parent, $parentPath)
const RELATION_PARENT
Interface ilDBInterface.
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...
Create styles array
The data for the language used.
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.