PathFilter - return right neighbours of search terms
[tine20] / tine20 / Tinebase / Record / Path.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  Record
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
10  * 
11  */
12
13 /**
14  * controller for record paths
15  *
16  * @package     Tinebase
17  * @subpackage  Record
18  */
19 class Tinebase_Record_Path extends Tinebase_Controller_Record_Abstract
20 {
21     /**
22      * @var Tinebase_Backend_Sql
23      */
24     protected $_backend;
25     
26     /**
27      * Model name
28      *
29      * @var string
30      */
31     protected $_modelName = 'Tinebase_Model_Path';
32     
33     /**
34      * check for container ACLs?
35      *
36      * @var boolean
37      */
38     protected $_doContainerACLChecks = FALSE;
39     
40     /**
41      * holds the instance of the singleton
42      *
43      * @var Tinebase_Alarm
44      */
45     private static $instance = NULL;
46
47     protected $_rebuildQueue = array();
48
49     protected $_afterRebuildQueueHook = array();
50
51     protected $_recursionCounter = 0;
52     
53     /**
54      * the constructor
55      *
56      */
57     private function __construct()
58     {
59         $this->_backend = new Tinebase_Path_Backend_Sql();
60     }
61     
62     /**
63      * the singleton pattern
64      *
65      * @return Tinebase_Record_Path
66      */
67     public static function getInstance() 
68     {
69         if (self::$instance === NULL) {
70             self::$instance = new self();
71         }
72         return self::$instance;
73     }
74
75     public function addAfterRebuildQueueHook(array $hook)
76     {
77         $this->_afterRebuildQueueHook[$this->_recursionCounter][] = $hook;
78     }
79
80     public function addToRebuildQueue(array $_rebuildPathParams)
81     {
82         // attention, this code contains recursion prevention logic, do not change this, except you understand that logic!
83         // see __CLASS__::_workRebuildQueue function
84         $shadowPathPart = $_rebuildPathParams[0]->getShadowPathPart();
85         if (!isset($this->_rebuildQueue[$shadowPathPart])) {
86             $this->_rebuildQueue[$shadowPathPart] = $_rebuildPathParams;
87         }
88     }
89
90     /**
91      * getPathsForRecords
92      *
93      * no acl check done in here
94      *
95      * @param Tinebase_Record_Interface $_record
96      * @return Tinebase_Record_RecordSet
97      */
98     public function getPathsForRecord(Tinebase_Record_Interface $_record)
99     {
100         $filter = new Tinebase_Model_PathFilter(array(
101             array('field' => 'shadow_path', 'operator' => 'contains', 'value' => $_record->getShadowPathPart())
102         ));
103
104         return $this->_backend->search($filter);
105     }
106
107     /**
108      * getPathsForShadowPathPart
109      *
110      * no acl check done in here
111      *
112      * @param string $_shadowPathPart
113      * @return Tinebase_Record_RecordSet
114      */
115     public function getPathsForShadowPathPart($_shadowPathPart)
116     {
117         $filter = new Tinebase_Model_PathFilter(array(
118             array('field' => 'shadow_path', 'operator' => 'contains', 'value' => $_shadowPathPart)
119         ));
120
121         return $this->_backend->search($filter);
122     }
123
124     /**
125      * @param Tinebase_Record_Interface $record
126      * @param string $oldPathPart
127      */
128     public function pathReplace(Tinebase_Record_Interface $record, $oldPathPart)
129     {
130         $paths = $this->getPathsForRecord($record);
131         $newPathPart = $record->getPathPart();
132
133         /** @var Tinebase_Model_Path $path */
134         foreach($paths as $path) {
135             if (false === ($pos = mb_strpos($path->path, $oldPathPart))) {
136                 throw new Tinebase_Exception_UnexpectedValue('could not find old part part: ' . $oldPathPart . ' in path: ' . $path->path);
137             }
138             if (false !== mb_strpos($path->path, $oldPathPart, $pos + 1)) {
139                 // TODO split by /, find right part, replace it, glue it with /
140                 // TODO write test for this code path!!!!
141             } else {
142                 $path->path = str_replace($oldPathPart, $newPathPart, $path->path);
143             }
144
145             $this->_backend->update($path);
146         }
147     }
148
149     /**
150      * @param Tinebase_Record_Interface $_record
151      * @param Tinebase_Record_Interface|null $_oldRecord the record before the update including relatedData / relations (but only those visible to the current user)
152      * @throws Tinebase_Exception_UnexpectedValue
153      */
154     public function rebuildPaths(Tinebase_Record_Interface $_record, Tinebase_Record_Interface $_oldRecord = null)
155     {
156         $this->_recursionCounter++;
157
158         if (null !== $_oldRecord && $_record->getId() !== $_oldRecord->getId()) {
159             throw new Tinebase_Exception_UnexpectedValue('id of current and updated record must not change');
160         }
161
162         if (null !== $_oldRecord && ($oldPathPart = $_oldRecord->getPathPart()) !== $_record->getPathPart()) {
163             $this->pathReplace($_record, $oldPathPart);
164         }
165
166         $ownShadowPathPart = $_record->getShadowPathPart();
167
168         try {
169             $pathNeighbours = $_record->getPathNeighbours();
170         } catch (Tinebase_Exception_Record_StopPathBuild $e) {
171             $this->_recursionCounter--;
172             return;
173         }
174
175
176         $paths = $this->getPathsForRecord($_record);
177         $pathsPathNeighbours = $paths->__call('getNeighbours', array($ownShadowPathPart));
178         $oldPathParents = array();
179         $oldPathChildren = array();
180         foreach($pathsPathNeighbours as $neighbours) {
181             if(isset($neighbours['parent'])) {
182                 $oldPathParents[$neighbours['parent']] = true;
183             }
184             if(isset($neighbours['child'])) {
185                 $oldPathChildren[$neighbours['child']] = true;
186             }
187         }
188
189         $newParents = array();
190         /** @var Tinebase_Record_Interface $parent */
191         foreach ($pathNeighbours['parents'] as $parent) {
192             $pathPart = trim($parent->getShadowPathPart(null, $_record), '/');
193             if (isset($oldPathParents[$pathPart])) {
194                 unset($oldPathParents[$pathPart]);
195             } else {
196                 if (isset($newParents[$pathPart])) {
197                     throw new Tinebase_Exception_UnexpectedValue('generated path part twice! ' . $pathPart);
198                 }
199                 $newParents[$pathPart] = $parent;
200             }
201         }
202
203         $newChildren = array();
204         /** @var Tinebase_Record_Interface $child */
205         foreach ($pathNeighbours['children'] as $child) {
206             $pathPart = $child->getShadowPathPart($_record);
207             if (isset($oldPathChildren[$pathPart])) {
208                 unset($oldPathChildren[$pathPart]);
209             } else {
210                 if (isset($newChildren[$pathPart])) {
211                     throw new Tinebase_Exception_UnexpectedValue('generated path part twice! ' . $pathPart);
212                 }
213                 $newChildren[$pathPart] = $child;
214             }
215         }
216
217
218         if (count($oldPathChildren) > 0 || count($oldPathParents) > 0) {
219             $toDelete = array();
220             foreach($oldPathParents as $pathPart => $tmp) {
221                 $toDelete[] = $pathPart . $ownShadowPathPart;
222             }
223             foreach($oldPathChildren as $pathPart => $tmp) {
224                 $toDelete[] = trim($ownShadowPathPart, '/') . $pathPart;
225             }
226
227             $this->deleteShadowPathParts($toDelete);
228
229         } else {
230
231             foreach ($newChildren as $child) {
232                 $this->addPathChild($_record, $child);
233             }
234             foreach ($newParents as $parent) {
235                 $this->addPathParent($_record, $parent);
236             }
237         }
238
239         $this->_workRebuildQueue();
240
241         $this->_recursionCounter--;
242         if (0 === $this->_recursionCounter)
243         {
244             $this->_rebuildQueue = array();
245         }
246     }
247
248     protected function _workRebuildQueue()
249     {
250         if (!empty($this->_rebuildQueue)) {
251             //attention this is recursion prevention logic, don't change this light headed
252             $queue = array_values($this->_rebuildQueue);
253             $this->_rebuildQueue = array_fill_keys(array_keys($this->_rebuildQueue), false);
254
255             foreach($queue as $params) {
256                 if (false !== $params) {
257                     call_user_func_array(array($this, 'rebuildPaths'), $params);
258                 }
259             }
260         }
261
262         if (isset($this->_afterRebuildQueueHook[$this->_recursionCounter])) {
263             // recursion prevention!
264             $hooks = $this->_afterRebuildQueueHook[$this->_recursionCounter];
265             unset($this->_afterRebuildQueueHook[$this->_recursionCounter]);
266             foreach($hooks as $hook) {
267                 call_user_func_array(array_shift($hook), $hook);
268             }
269         }
270     }
271
272     /**
273      * @param Tinebase_Record_RecordSet $_paths
274      * @param Tinebase_Record_Interface $_record
275      * @param string|null $_recordShadowPathPart
276      * @return array
277      * @throws Tinebase_Exception_UnexpectedValue
278      */
279     protected function _getUniqueTreeTail(Tinebase_Record_RecordSet $_paths, Tinebase_Record_Interface $_record, $_recordShadowPathPart = null)
280     {
281         if (null === $_recordShadowPathPart) {
282             $_recordShadowPathPart = $_record->getShadowPathPart();
283         }
284
285         $uniquePaths = array();
286         /** @var Tinebase_Model_Path $path */
287         foreach ($_paths as $path) {
288             if (false === ($pos = strpos($path->shadow_path, $_recordShadowPathPart))) {
289                 throw new Tinebase_Exception_UnexpectedValue('shadow path: ' . $path->shadow_path . ' doesn\'t contain: ' . $_recordShadowPathPart);
290             }
291             $tailPart = substr($path->shadow_path, $pos);
292
293             if (!isset($uniquePaths[$tailPart])) {
294                 $pathDept = count(explode('/', trim($tailPart, '/')));
295                 $pathParts = array_reverse(explode('/', trim($path->path, '/')));
296                 $newPath = '';
297                 $i = 0;
298                 while($i < $pathDept) {
299                     $newPath = '/' . $pathParts[$i++] . $newPath;
300                 }
301
302                 $uniquePaths[$tailPart] = array('path' => $newPath, 'record' => $path);
303             }
304         }
305         if (count($uniquePaths) === 0) {
306             $pathPart = $_record->getPathPart();
307             $uniquePaths[$_recordShadowPathPart] = array('path' => $pathPart);
308         }
309
310         return $uniquePaths;
311     }
312
313     /**
314      * @param Tinebase_Record_RecordSet $_paths
315      * @param Tinebase_Record_Interface $_record
316      * @param string|null $_recordShadowPathPart
317      * @return array
318      * @throws Tinebase_Exception_UnexpectedValue
319      */
320     protected function _getUniqueTreeHead(Tinebase_Record_RecordSet $_paths, Tinebase_Record_Interface $_record, $_recordShadowPathPart = null)
321     {
322         if (null === $_recordShadowPathPart) {
323             $_recordShadowPathPart = $_record->getShadowPathPart();
324         }
325         $lengthShadowPathPart = strlen($_recordShadowPathPart);
326
327         $uniquePaths = array();
328         /** @var Tinebase_Model_Path $path */
329         foreach ($_paths as $path) {
330             if (false === ($pos = strpos($path->shadow_path, $_recordShadowPathPart))) {
331                 throw new Tinebase_Exception_UnexpectedValue('shadow path: ' . $path->shadow_path . ' doesn\'t contain: ' . $_recordShadowPathPart);
332             }
333             $headPart = substr($path->shadow_path, 0, $pos + $lengthShadowPathPart);
334
335             if (!isset($uniquePaths[$headPart])) {
336                 $pathDept = count(explode('/', trim($headPart, '/')));
337                 $pathParts = explode('/', trim($path->path, '/'));
338                 $newPath = '';
339                 $i = 0;
340                 while($i < $pathDept) {
341                     $newPath .= '/' . $pathParts[$i++];
342                 }
343
344                 // remove last {TYPE} if present
345                 if (false !== ($pos = strrpos($newPath, '}')) && $pos === strlen($newPath) - 1) {
346                     $newPath = substr($newPath, 0, strrpos($newPath, '{'));
347                 }
348
349                 $uniquePaths[$headPart] = array('path' => $newPath, 'record' => $path);
350             }
351         }
352         if (count($uniquePaths) === 0) {
353             $pathPart = $_record->getPathPart();
354             $uniquePaths[$_recordShadowPathPart] = array('path' => $pathPart);
355         }
356
357         return $uniquePaths;
358     }
359
360     /**
361      * @param array $uniquePaths
362      * @param array $uniqueChildPaths
363      * @param string $pathType
364      * @param string $_recordShadowPathPart
365      * @throws Tinebase_Exception_UnexpectedValue
366      */
367     protected function _joinPathTrees(array $_uniquePaths, array $_uniqueChildPaths, $_pathType, $_recordShadowPathPart)
368     {
369         foreach($_uniquePaths as $shadowPathPart => $data) {
370             $reUsePath = (isset($data['record']) && $data['record']->shadow_path === $shadowPathPart);
371
372             foreach($_uniqueChildPaths as $childShadowPathPart => $childData) {
373                 if (isset($childData['record']) && $childData['record']->shadow_path === $childShadowPathPart) {
374                     $path = $childData['record'];
375                 } elseif (true === $reUsePath) {
376                     $path = $data['record'];
377                     $reUsePath = false;
378                 } else {
379                     $path = new Tinebase_Model_Path(array(), true);
380                 }
381                 $path->path = $data['path'] . $_pathType . $childData['path'];
382                 $path->shadow_path = $shadowPathPart . $_pathType . $childShadowPathPart;
383
384                 if (($count = substr_count($path->shadow_path, $_recordShadowPathPart)) !== 1) {
385                     throw new Tinebase_Exception_UnexpectedValue('newly created shadow path: ' . $path->shadow_path . ' contains ' . $count . ' times the records shadow path: ' . $_recordShadowPathPart);
386                 }
387
388                 if (!empty($path->getId())) {
389                     $this->_backend->update($path);
390                 } else {
391                     $this->_backend->create($path);
392                 }
393             }
394
395             if (true === $reUsePath) {
396                 $this->_backend->delete($data['record']->getId());
397             }
398         }
399     }
400
401     /**
402      * @param Tinebase_Record_Interface $_record
403      * @param Tinebase_Record_Interface $_child
404      * @throws Tinebase_Exception_UnexpectedValue
405      */
406     public function addPathChild($_record, $_child)
407     {
408         $recordShadowPathPart = $_record->getShadowPathPart();
409         $childShadowPathPart = $_child->getShadowPathPart();
410
411         $paths = $this->getPathsForShadowPathPart($recordShadowPathPart);
412         $childPaths = $this->getPathsForShadowPathPart($childShadowPathPart);
413
414
415         if ($paths->count() === 0 && $childPaths->count() === 0) {
416             $path = new Tinebase_Model_Path(array(
417                 'path'          => $_record->getPathPart() . $_child->getPathPart($_record),
418                 'shadow_path'   => $recordShadowPathPart . $_child->getShadowPathPart($_record)
419             ));
420             $this->_backend->create($path);
421             return;
422         }
423
424
425         $uniqueChildPaths = $this->_getUniqueTreeTail($childPaths, $_child, $childShadowPathPart);
426         $uniquePaths = $this->_getUniqueTreeHead($paths, $_record, $recordShadowPathPart);
427
428         $pathType = $_child->getTypeForPathPart();
429
430         $this->_joinPathTrees($uniquePaths, $uniqueChildPaths, $pathType, $recordShadowPathPart);
431     }
432
433     /**
434      * @param Tinebase_Record_Interface $_record
435      * @param Tinebase_Record_Interface $_parent
436      * @throws Tinebase_Exception_UnexpectedValue
437      */
438     public function addPathParent($_record, $_parent)
439     {
440         $recordShadowPathPart = $_record->getShadowPathPart();
441         $parentShadowPathPart = $_parent->getShadowPathPart();
442
443         $paths = $this->getPathsForShadowPathPart($recordShadowPathPart);
444         $parentPaths = $this->getPathsForShadowPathPart($parentShadowPathPart);
445
446
447         if ($paths->count() === 0 && $parentPaths->count() === 0) {
448             $path = new Tinebase_Model_Path(array(
449                 'path'          => $_parent->getPathPart() . $_record->getPathPart($_parent),
450                 'shadow_path'   => $parentShadowPathPart . $_record->getShadowPathPart($_parent)
451             ));
452             $this->_backend->create($path);
453             return;
454         }
455
456         $uniqueChildPaths = $this->_getUniqueTreeTail($paths, $_record, $recordShadowPathPart);
457         $uniquePaths = $this->_getUniqueTreeHead($parentPaths, $_parent, $parentShadowPathPart);
458
459         $pathType = $_parent->getTypeForPathPart();
460
461         $this->_joinPathTrees($uniquePaths, $uniqueChildPaths, $pathType, $recordShadowPathPart);
462     }
463
464     /**
465      * @param array $_shadowPathPart
466      */
467     public function deleteShadowPathParts(array $_shadowPathParts)
468     {
469         $ids = array();
470         $paths = array();
471         foreach ($_shadowPathParts as $shadowPathPart) {
472             $filter = new Tinebase_Model_PathFilter(array(
473                 array('field' => 'shadow_path', 'operator' => 'contains', 'value' => $shadowPathPart)
474             ));
475             $paths = $this->_backend->search($filter);
476             foreach($paths as $path) {
477                 $ids[$path->getId()] = true;
478             }
479         }
480
481         $this->_backend->delete(array_keys($ids));
482
483         $this->_workRebuildQueue();
484
485         $recordIds = array();
486         /** @var Tinebase_Model_Path $path */
487         foreach($paths as $path) {
488             foreach($path->getRecordIds() as $key => $data) {
489                 if (!isset($recordIds[$key])) {
490                     $recordIds[$key] = $data;
491                 }
492             }
493         }
494
495         $controllerCache = array();
496         foreach($recordIds as $data) {
497             $model = $data['model'];
498             if (isset($controllerCache[$model])) {
499                 $controller = $controllerCache[$model];
500             } else {
501                 $controller = $controllerCache[$model] = Tinebase_Core::getApplicationInstance($model);
502             }
503
504             try {
505                 $record = $controller->get($data['id']);
506             } catch(Tinebase_Exception_NotFound $tenf) {
507                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
508                     . ' could not get record during path rebuild, possibly concurrent deletion');
509                 continue;
510             }
511
512             $this->rebuildPaths($record);
513         }
514     }
515 }