ILIAS  release_5-2 Revision v5.2.25-18-g3f80b828510
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  {
73  $childs[] = $row['child'];
74  }
75  return $childs ? $childs : array();
76  }
77 
84  public function getRelation($a_node_a, $a_node_b)
85  {
86  if($a_node_a['child'] == $a_node_b['child'])
87  {
88  ilLoggerFactory::getLogger('tree')->debug('EQUALS');
90  }
91  if(stripos($a_node_a['path'], $a_node_b['path']. '.') === 0)
92  {
93  ilLoggerFactory::getLogger('tree')->debug('CHILD');
95  }
96  if(stripos($a_node_b['path'], $a_node_a['path'] . '.') === 0)
97  {
98  ilLoggerFactory::getLogger('tree')->debug('PARENT');
100  }
101  $path_a = substr($a_node_a['path'],0,strrpos($a_node_a['path'],'.'));
102  $path_b = substr($a_node_b['path'],0,strrpos($a_node_b['path'],'.'));
103 
104  ilLoggerFactory::getLogger('tree')->debug('Comparing '.$path_a .' '. 'with '.$path_b);
105 
106  if($a_node_a['path'] and (strcmp($path_a,$path_b) === 0))
107  {
108  ilLoggerFactory::getLogger('tree')->debug('SIBLING');
110  }
111 
112  ilLoggerFactory::getLogger('tree')->debug('NONE');
113  return ilTree::RELATION_NONE;
114  }
115 
125  public function getSubTreeQuery($a_node, $a_types = '', $a_force_join_reference = true, $a_fields = array())
126  {
127  global $ilDB;
128 
129  $type_str = '';
130  if(is_array($a_types))
131  {
132  if($a_types)
133  {
134  $type_str = "AND ".$ilDB->in($this->getTree()->getObjectDataTable().".type", $a_types, false, "text");
135  }
136  }
137  else if(strlen($a_types))
138  {
139  $type_str = "AND ".$this->getTree()->getObjectDataTable().".type = ".$ilDB->quote($a_types, "text");
140  }
141 
142  $join = '';
143  if($type_str or $a_force_join_reference)
144  {
145  $join = $this->getTree()->buildJoin();
146  }
147 
148  $fields = '* ';
149  if(count($a_fields))
150  {
151  $fields = implode(',',$a_fields);
152  }
153 
154  // @todo order by
155  $query = 'SELECT '.
156  $fields.' '.
157  'FROM '.$this->getTree()->getTreeTable().' '.
158  $join.' '.
159  'WHERE '.$this->getTree()->getTreeTable().'.path '.
160  'BETWEEN '.
161  $ilDB->quote($a_node['path'],'text').' AND '.
162  $ilDB->quote($a_node['path'].'.Z','text').' '.
163  'AND '.$this->getTree()->getTreeTable().'.'.$this->getTree()->getTreePk().' = '.$ilDB->quote($this->getTree()->getTreeId(),'integer').' '.
164  $type_str.' '.
165  'ORDER BY '.$this->getTree()->getTreeTable().'.path';
166 
167  return $query;
168  }
169 
176  public function getPathIds($a_endnode, $a_startnode = 0)
177  {
178  global $ilDB;
179 
180  $ilDB->setLimit(1);
181  $query = 'SELECT path FROM ' . $this->getTree()->getTreeTable() .' '.
182  'WHERE child = '. $ilDB->quote($a_endnode,'integer').' ';
183  $res = $ilDB->query($query);
184 
185  $path = null;
186  while ($row = $ilDB->fetchAssoc($res))
187  {
188  $path = $row['path'];
189  }
190 
191  $pathIds = explode('.', $path);
192 
193  if ($a_startnode != 0)
194  {
195  while (count($pathIds) > 0 && $pathIds[0] != $a_startnode)
196  {
197  array_shift($pathIds);
198  }
199  }
200  return $pathIds;
201  }
202 
211  public function insertNode($a_node_id, $a_parent_id, $a_pos)
212  {
213  global $ilDB;
214 
215  $insert_node_callable = function(ilDBInterface $ilDB) use ($a_node_id, $a_parent_id, $a_pos)
216  {
217  // get path and depth of parent
218  $ilDB->setLimit(1);
219 
220  $res = $ilDB->queryF(
221  'SELECT parent, depth, path FROM ' . $this->getTree()->getTreeTable() . ' ' .
222  'WHERE child = %s '. ' '.
223  'AND ' . $this->getTree()->getTreePk() . ' = %s', array('integer', 'integer'),
224  array($a_parent_id, $this->getTree()->getTreeId()));
225 
226 
227  $r = $ilDB->fetchObject($res);
228 
229  if ($r->parent === NULL)
230  {
232  throw new ilInvalidTreeStructureException('Parent node not found in tree');
233 
234  }
235 
236  if ($r->depth >= $this->getMaximumPossibleDepth())
237  {
239  throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
240  }
241 
242  $parentPath = $r->path;
243  $depth = $r->depth + 1;
244  $lft = 0;
245  $rgt = 0;
246 
247 
248  $ilDB->insert($this->getTree()->getTreeTable(), array($this->getTree()->getTreePk() => array('integer', $this->getTree()->getTreeId()),
249  'child' => array('integer', $a_node_id),
250  'parent' => array('integer', $a_parent_id),
251  'lft' => array('integer', $lft),
252  'rgt' => array('integer', $rgt),
253  'depth' => array('integer', $depth),
254  'path' => array('text', $parentPath . "." . $a_node_id)));
255 
256  };
257 
258  // use ilAtomQuery to lock tables if tree is main tree
259  // otherwise just call this closure without locking
260  if ($this-> getTree()->__isMainTree())
261  {
262  $ilAtomQuery = $ilDB->buildAtomQuery();
263  $ilAtomQuery->addTableLock("tree");
264 
265  $ilAtomQuery->addQueryCallable($insert_node_callable);
266 
267  $ilAtomQuery->run();
268  }
269  else
270  {
271  $insert_node_callable($ilDB);
272  }
273  }
274 
275 
282  public function deleteTree($a_node_id)
283  {
284  global $ilDB;
285  $delete_tree_callable = function(ilDBInterface $ilDB) use($a_node_id)
286  {
287  $query = 'SELECT * FROM '.$this->getTree()->getTreeTable().' '.
288  'WHERE '.$this->getTree()->getTreeTable().'.child = %s '.
289  'AND '.$this->getTree()->getTreeTable().'.'.$this->getTree()->getTreePk().' = %s ';
290  $res = $ilDB->queryF($query,array('integer','integer'),array(
291  $a_node_id,
292  $this->getTree()->getTreeId()));
293  $row = $ilDB->fetchAssoc($res);
294 
295  $query = 'DELETE FROM '.$this->getTree()->getTreeTable().' '.
296  'WHERE path BETWEEN '.$ilDB->quote($row['path'],'text').' '.
297  'AND '.$ilDB->quote($row['path'].'.Z','text').' '.
298  'AND '.$this->getTree()->getTreePk().' = '.$ilDB->quote($this->getTree()->getTreeId(), 'integer');
299  $ilDB->manipulate($query);
300  };
301 
302  // get lft and rgt values. Don't trust parameter lft/rgt values of $a_node
303  if($this->getTree()->__isMainTree())
304  {
305  $ilAtomQuery = $ilDB->buildAtomQuery();
306  $ilAtomQuery->addTableLock('tree');
307  $ilAtomQuery->addQueryCallable($delete_tree_callable);
308  $ilAtomQuery->run();
309  }
310  else
311  {
312  $delete_tree_callable($ilDB);
313  }
314 
315  return true;
316  }
317 
324  public function moveToTrash($a_node_id)
325  {
326  global $ilDB;
327 
328  $move_to_trash_callable = function(ilDBInterface $ilDB) use($a_node_id)
329  {
330  $node = $this->getTree()->getNodeTreeData($a_node_id);
331 
332  // Set the nodes deleted (negative tree id)
333  $ilDB->manipulateF('
334  UPDATE ' . $this->getTree()->getTreeTable().' '.
335  'SET tree = %s' .' '.
336  'WHERE ' . $this->getTree()->getTreePk() . ' = %s ' .
337  'AND path BETWEEN %s AND %s',
338  array('integer', 'integer', 'text', 'text'),
339  array(-$a_node_id, $this->getTree()->getTreeId(), $node['path'], $node['path'] . '.Z'));
340 
341  };
342 
343  // use ilAtomQuery to lock tables if tree is main tree
344  // otherwise just call this closure without locking
345  if ($this->getTree()->__isMainTree())
346  {
347  $ilAtomQuery = $ilDB->buildAtomQuery();
348  $ilAtomQuery->addTableLock("tree");
349 
350  $ilAtomQuery->addQueryCallable($move_to_trash_callable);
351 
352  $ilAtomQuery->run();
353  }
354  else
355  {
356  $move_to_trash_callable($ilDB);
357  }
358 
359  return true;
360  }
361 
371  public function moveTree($a_source_id, $a_target_id, $a_position)
372  {
373  global $ilDB;
374 
375  $move_tree_callable = function(ilDBInterface $ilDB) use ($a_source_id, $a_target_id, $a_position)
376  {
377  // Receive node infos for source and target
378  $ilDB->setLimit(2);
379 
380  $res = $ilDB->query(
381  'SELECT depth, child, parent, path FROM ' . $this->getTree()->getTreeTable() . ' '.
382  'WHERE ' . $ilDB->in('child', array($a_source_id, $a_target_id), false, 'integer') . ' '.
383  'AND tree = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer')
384  );
385 
386  // Check in tree
387  if ($ilDB->numRows($res) != 2)
388  {
389  ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
390  throw new InvalidArgumentException('Error moving subtree');
391  }
392 
393  while ($row = $ilDB->fetchObject($res))
394  {
395  if ($row->child == $a_source_id)
396  {
397  $source_path = $row->path;
398  $source_depth = $row->depth;
399  $source_parent = $row->parent;
400  }
401  else
402  {
403  $target_path = $row->path;
404  $target_depth = $row->depth;
405  }
406  }
407 
408  if ($target_depth >= $source_depth)
409  {
410  // We move nodes deeper into the tree. Therefore we need to
411  // check whether we might exceed the maximal path length.
412  // We use FOR UPDATE here, because we don't want anyone to
413  // insert new nodes while we move the subtree.
414 
415  $res = $ilDB->queryF(
416  'SELECT MAX(depth) max_depth '.
417  'FROM ' . $this->getTree()->getTreeTable() . ' '.
418  'WHERE path BETWEEN %s AND %s'.' '.
419  'AND tree = %s ',
420  array('text', 'text', 'integer'), array($source_path, $source_path . '.Z', $this->getTree()->getTreeId()));
421 
422  $row = $ilDB->fetchObject($res);
423 
424  if ($row->max_depth - $source_depth + $target_depth + 1 > $this->getMaximumPossibleDepth())
425  {
426  ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
427  throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
428  }
429  }
430  // Check target not child of source
431  if (substr($target_path . '.', 0, strlen($source_path) . '.') == $source_path . '.')
432  {
433  ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Target is child of source');
434  throw new InvalidArgumentException('Error moving subtree: target is child of source');
435  }
436  $depth_diff = $target_depth - $source_depth + 1;
437 
438  // move subtree:
439  $query = '
440  UPDATE ' . $this->getTree()->getTreeTable() . '
441  SET parent = CASE WHEN parent = ' . $ilDB->quote($source_parent, 'integer') . '
442  THEN ' . $ilDB->quote($a_target_id, 'integer') . '
443  ELSE parent END,
444 
445  path = ' . $ilDB->concat(array(
446  array($ilDB->quote($target_path, 'text'), 'text'),
447  array($ilDB->substr('path', strrpos('.' . $source_path, '.')), 'text'))) . ' ,
448 
449  depth = depth + ' . $ilDB->quote($depth_diff, 'integer') . '
450 
451  WHERE path BETWEEN ' . $ilDB->quote($source_path, 'text') . '
452  AND ' . $ilDB->quote($source_path . '.Z', 'text') . '
453 
454  AND tree = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer');
455 
456  ilLoggerFactory::getLogger('tree')->debug('Query is ' . $query);
457 
458  $ilDB->manipulate($query);
459  };
460 
461 
462  if ($this->getTree()->__isMainTree())
463  {
464  $ilAtomQuery = $ilDB->buildAtomQuery();
465  $ilAtomQuery->addTableLock("tree");
466  $ilAtomQuery->addQueryCallable($move_tree_callable);
467  $ilAtomQuery->run();
468  }
469  else{
470  $move_tree_callable($ilDB);
471  }
472 
473  return true;
474  }
475 
476 
477  public static function createFromParentReleation()
478  {
479  global $ilDB;
480 
481  $r = $ilDB->queryF('SELECT DISTINCT * FROM tree WHERE parent = %s', array('integer'), array(0));
482 
483  while ($row = $ilDB->fetchAssoc($r))
484  {
485  $success = self::createMaterializedPath(0, '');
486 
487  if ($success !== true)
488  {
489  }
490  }
491  }
492 
498  private static function createMaterializedPath($parent, $parentPath)
499  {
500  global $ilDB;
501  $q = ' UPDATE tree
502  SET path = CONCAT(COALESCE(' . $ilDB->quote($parentPath, 'text') . ', \'\'), COALESCE( ' . $ilDB->cast("child","text") . ' , \'\'))
503  WHERE parent = %s';
504  $r = $ilDB->manipulateF($q, array('integer'), array($parent));
505 
506  $r = $ilDB->queryF('SELECT child FROM tree WHERE parent = %s', array('integer'), array($parent));
507 
508  while ($row = $ilDB->fetchAssoc($r))
509  {
510  self::createMaterializedPath($row['child'], $parentPath . $row['child'] . '.');
511  }
512 
513  return true;
514  }
515 
520  public function getSubtreeInfo($a_endnode_id)
521  {
522  global $ilDB;
523 
524  // This is an optimization without the temporary tables become too big for our system.
525  // The idea is to use a subquery to join and filter the trees, and only the result
526  // is joined to obj_reference and obj_data.
527 
528  $query = "SELECT t2.child child, type, t2.path path " .
529  "FROM " . $this->getTree()->getTreeTable() . " t1 " .
530  "JOIN " . $this->getTree()->getTreeTable() . " t2 ON (t2.path BETWEEN t1.path AND CONCAT(t1.path, '.Z')) " .
531  "JOIN " . $this->getTree()->getTableReference() . " obr ON t2.child = obr.ref_id " .
532  "JOIN " . $this->getTree()->getObjectDataTable() . " obd ON obr.obj_id = obd.obj_id " .
533  "WHERE t1.child = " . $ilDB->quote($a_endnode_id, 'integer') . " " .
534  "AND t1." . $this->getTree()->getTreePk() . " = " . $ilDB->quote($this->getTree()->getTreeId(), 'integer') . " " .
535  "AND t2." . $this->getTree()->getTreePk() . " = " . $ilDB->quote($this->getTree()->getTreeId(), 'integer') . " " .
536  "ORDER BY t2.path";
537 
538 
539  $res = $ilDB->query($query);
540  $nodes = array();
541  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT))
542  {
543  #$nodes[$row->child]['lft'] = $row->lft;
544  #$nodes[$row->child]['rgt'] = $row->rgt;
545  $nodes[$row->child]['child'] = $row->child;
546  $nodes[$row->child]['type'] = $row->type;
547  $nodes[$row->child]['path'] = $row->path;
548  }
549 
550  $depth_first_compare = function($a, $b)
551  {
552  $a_exploded = explode('.', $a['path']);
553  #ilLoggerFactory::getLogger('tree')->debug(print_r($a_exploded,TRUE));
554  $b_exploded = explode('.', $b['path']);
555 
556  $a_padded = '';
557  foreach($a_exploded as $num)
558  {
559  $a_padded .= (str_pad((string) $num, 14,'0', STR_PAD_LEFT));
560  }
561  $b_padded = '';
562  foreach($b_exploded as $num)
563  {
564  $b_padded .= (str_pad((string) $num, 14, '0', STR_PAD_LEFT));
565  }
566 
567  #ilLoggerFactory::getLogger('tree')->debug($a_padded);
568  return strcasecmp($a_padded, $b_padded);
569  };
570 
571  #ilLoggerFactory::getLogger('tree')->debug(print_r($nodes,TRUE));
572 
573  uasort($nodes,$depth_first_compare);
574 
575  #ilLoggerFactory::getLogger('tree')->debug(print_r($nodes,TRUE));
576 
577  return (array) $nodes;
578  }
579 
584  public function validateParentRelations()
585  {
586  global $ilDB;
587 
588  $query = 'select child from '.$this->getTree()->getTreeTable().' child where not exists '.
589  '( '.
590  'select child from '.$this->getTree()->getTreeTable().' parent where child.parent = parent.child and '.
591  '(child.path BETWEEN parent.path AND CONCAT(parent.path,'.$ilDB->quote('Z','text').') )'. ')'.
592  'and '.$this->getTree()->getTreePk().' = '.$this->getTree()->getTreeId().' and child <> 1';
593  $res = $ilDB->query($query);
594 
595  ilLoggerFactory::getLogger('tree')->debug($query);
596 
597  $failures = array();
598  while($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC))
599  {
600  $failures[] = $row[$this->getTree()->getTreePk()];
601  }
602  return $failures;
603  }
604 }
605 
606 ?>
Thrown if invalid tree strucutes are found.
$path
Definition: aliased.php:25
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
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.
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.