0011562: adding two new tasks fails when saving lead
[tine20] / tine20 / Tinebase / Relations.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Relations
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2008-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  * 
11  * @todo        re-enable the caching (but check proper invalidation first) -> see task #232
12  */
13
14 /**
15  * Class for handling relations between application records.
16  * @todo move json api specific stuff into the model
17  * 
18  * @package     Tinebase
19  * @subpackage  Relations 
20  */
21 class Tinebase_Relations
22 {
23     /**
24      * @var Tinebase_Relation_Backend_Sql
25      */
26     protected $_backend;
27     /**
28      * holds the instance of the singleton
29      *
30      * @var Tinebase_Relations
31      */
32     private static $instance = NULL;
33     
34     /**
35      * the constructor
36      *
37      */
38     private function __construct()
39     {
40         $this->_backend = new Tinebase_Relation_Backend_Sql();
41     }
42     
43     /**
44      * the singleton pattern
45      *
46      * @return Tinebase_Relations
47      */
48     public static function getInstance() 
49     {
50         if (self::$instance === NULL) {
51             self::$instance = new Tinebase_Relations();
52         }
53         return self::$instance;
54     }
55     
56     /**
57      * set all relations of a given record
58      * 
59      * NOTE: given relation data is expected to be an array atm.
60      * @todo check read ACL for new relations to existing records.
61      * 
62      * @param  string $_model           own model to get relations for
63      * @param  string $_backend         own backend to get relations for
64      * @param  string $_id              own id to get relations for 
65      * @param  array  $_relationData    data for relations to create
66      * @param  bool   $_ignoreACL       create relations without checking permissions
67      * @param  bool   $_inspectRelated  do update/create related records on the fly
68      * @param  bool   $_doCreateUpdateCheck do duplicate/freebusy/... checking for relations
69      * @return void
70      */
71     public function setRelations($_model,
72                                  $_backend,
73                                  $_id,
74                                  $_relationData,
75                                  $_ignoreACL = false,
76                                  $_inspectRelated = false,
77                                  $_doCreateUpdateCheck = false)
78     {
79         $relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
80         foreach((array) $_relationData as $relationData) {
81             if ($relationData instanceof Tinebase_Model_Relation) {
82                 $relations->addRecord($relationData);
83             } else {
84                 $relation = new Tinebase_Model_Relation(NULL, TRUE);
85                 $relation->setFromJsonInUsersTimezone($relationData);
86                 $relations->addRecord($relation);
87             }
88         }
89         
90         // own id sanitising
91         $relations->own_model   = $_model;
92         $relations->own_backend = $_backend;
93         $relations->own_id      = $_id;
94         
95         // convert related_record to record objects
96         // @todo move this to a relation json class / or to model->setFromJson
97         $this->_relatedRecordToObject($relations);
98         
99         // compute relations to add/delete
100         $currentRelations = $this->getRelations($_model, $_backend, $_id, NULL, array(), $_ignoreACL);
101         $currentIds   = $currentRelations->getArrayOfIds();
102         $relationsIds = $relations->getArrayOfIds();
103         
104         $toAdd = $relations->getIdLessIndexes();
105         $toDel = array_diff($currentIds, $relationsIds);
106         $toUpdate = array_intersect($currentIds, $relationsIds);
107
108         // prevent two empty related_id s of the same relation type
109         $emptyRelatedId = array();
110         foreach ($toAdd as $idx) {
111             if (empty($relations[$idx]->related_id)) {
112                 $relations[$idx]->related_id = Tinebase_Record_Abstract::generateUID();
113                 $emptyRelatedId[$idx] = true;
114             }
115         }
116         $this->_validateConstraintsConfig($_model, $relations, $toDel, $toUpdate);
117         
118         // break relations
119         foreach ($toDel as $relationId) {
120             $this->_backend->breakRelation($relationId);
121         }
122         
123         // add new relations
124         foreach ($toAdd as $idx) {
125             if(isset($emptyRelatedId[$idx])) {
126                 $relations[$idx]->related_id = null;
127                 $this->_setAppRecord($relations[$idx], $_doCreateUpdateCheck);
128             }
129             $this->_addRelation($relations[$idx]);
130         }
131         
132         // update relations
133         foreach ($toUpdate as $relationId) {
134             $current = $currentRelations[$currentRelations->getIndexById($relationId)];
135             $update = $relations[$relations->getIndexById($relationId)];
136             
137             // update related records if explicitly needed
138             if ($_inspectRelated) {
139                 // @todo do we need to omit so many fields?
140                 if (! $current->related_record->isEqual(
141                     $update->related_record, 
142                     array(
143                         'jpegphoto', 
144                         'creation_time', 
145                         'last_modified_time',
146                         'created_by',
147                         'last_modified_by',
148                         'is_deleted',
149                         'deleted_by',
150                         'deleted_time',
151                         'tags',
152                         'notes',
153                     )
154                 )) {
155                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
156                         . ' Related record diff: ' . print_r($current->related_record->diff($update->related_record)->toArray(), true));
157                     
158                     $this->_setAppRecord($update, $_doCreateUpdateCheck);
159                 }
160             }
161             
162             if (! $current->isEqual($update, array('related_record'))) {
163                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
164                     . ' Relation diff: ' . print_r($current->diff($update)->toArray(), true));
165                 
166                 $this->_updateRelation($update);
167             }
168         }
169     }
170     
171     /**
172      * returns the constraints config for the given models and their mirrored values (seen from the other side
173      * 
174      * @param array $models
175      * @return array
176      */
177     public static function getConstraintsConfigs($models)
178     {
179         if (! is_array($models)) {
180             $models = array($models);
181         }
182         $allApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name;
183         $ret = array();
184         
185         foreach ($models as $model) {
186         
187             $ownModel = explode('_Model_', $model);
188         
189             if (! class_exists($model) || ! in_array($ownModel[0], $allApplications)) {
190                 continue;
191             }
192             $cItems = $model::getRelatableConfig();
193             
194             $ownApplication = $ownModel[0];
195             $ownModel = $ownModel[1];
196         
197             if (is_array($cItems)) {
198                 foreach($cItems as $cItem) {
199         
200                     if (! array_key_exists('config', $cItem)) {
201                         continue;
202                     }
203         
204                     // own side
205                     $ownConfigItem = $cItem;
206                     $ownConfigItem['ownModel'] = $ownModel;
207                     $ownConfigItem['ownApp'] = $ownApplication;
208                     $ownConfigItem['ownRecordClassName'] = $ownApplication . '_Model_' . $ownModel;
209                     $ownConfigItem['relatedRecordClassName'] = $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'];
210                     
211                     $foreignConfigItem = array(
212                         'reverted'     => true,
213                         'ownApp'       => $cItem['relatedApp'],
214                         'ownModel'     => $cItem['relatedModel'],
215                         'relatedModel' => $ownModel,
216                         'relatedApp'   => $ownApplication,
217                         'default'      => array_key_exists('default', $cItem) ? $cItem['default'] : NULL,
218                         'ownRecordClassName' => $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'],
219                         'relatedRecordClassName' => $ownApplication . '_Model_' . $ownModel
220                     );
221         
222                     // KeyfieldConfigs
223                     if (array_key_exists('keyfieldConfig', $cItem)) {
224                         $foreignConfigItem['keyfieldConfig'] = $cItem['keyfieldConfig'];
225                         if ($cItem['keyfieldConfig']['from']){
226                             $foreignConfigItem['keyfieldConfig']['from'] = $cItem['keyfieldConfig']['from'] == 'foreign' ? 'own' : 'foreign';
227                         }
228                     }
229         
230                     $j=0;
231                     foreach ($cItem['config'] as $conf) {
232                         $max = explode(':',$conf['max']);
233                         $ownConfigItem['config'][$j]['max'] = intval($max[0]);
234         
235                         $foreignConfigItem['config'][$j] = $conf;
236                         $foreignConfigItem['config'][$j]['max'] = intval($max[1]);
237                         if ($conf['degree'] == 'sibling') {
238                             $foreignConfigItem['config'][$j]['degree'] = $conf['degree'];
239                         } else {
240                             $foreignConfigItem['config'][$j]['degree'] = $conf['degree'] == 'parent' ? 'child' : 'parent';
241                         }
242                         $j++;
243                     }
244                     
245                     $ret[] = $ownConfigItem;
246                     $ret[] = $foreignConfigItem;
247                 }
248             }
249         }
250         
251         return $ret;
252     }
253     
254     /**
255      * validate constraints from the own and the other side.
256      * this may be very expensive, if there are many constraints to check.
257      * 
258      * @param string $ownModel
259      * @param Tinebase_Record_RecordSet $relations
260      * @throws Tinebase_Exception_InvalidRelationConstraints
261      */
262     protected function _validateConstraintsConfig($ownModel, $relations, $toDelete = array(), $toUpdate = array())
263     {
264         if (! $relations->count()) {
265             return;
266         }
267         $relatedModels = array_unique($relations->related_model);
268         $relatedIds    = array_unique($relations->related_id);
269         
270         $toDelete      = is_array($toDelete) ? $toDelete : array();
271         $toUpdate      = is_array($toUpdate) ? $toUpdate : array();
272         $excludeCount  = array_merge($toDelete, $toUpdate);
273
274         $ownId         = $relations->getFirstRecord()->own_id;
275
276         // find out all models having a constraints config
277         $allModels = $relatedModels;
278         $allModels[] = $ownModel;
279         $allModels = array_unique($allModels);
280
281         $constraintsConfigs = self::getConstraintsConfigs($allModels);
282         $relatedConstraints = $this->_backend->countRelatedConstraints($ownModel, $relations, $excludeCount);
283         
284         $groups = array();
285         foreach($relations as $relation) {
286             $groups[] = $relation->related_model . '--' . $relation->type . '--' . $relation->own_id;
287         }
288         
289         $myConstraints = array_count_values($groups);
290
291         $groups = array();
292         foreach($relations as $relation) {
293             if (! in_array($relation->getId(), $excludeCount)) {
294                 $groups[] = $relation->own_model . '--' . $relation->type . '--' . $relation->related_id;
295             }
296         }
297         
298         foreach($relatedConstraints as $relC) {
299             for ($i = 0; $i < $relC['count']; $i++) {
300                 $groups[] = $relC['id'];
301             }
302         }
303         
304         $allConstraints = array_count_values($groups);
305
306         foreach ($constraintsConfigs as $cc) {
307             if (! isset($cc['config'])) {
308                 continue;
309             }
310             foreach($cc['config'] as $config) {
311                 
312                 $group = $cc['relatedRecordClassName'] . '--' . $config['type'];
313                 $idGroup = $group . '--' . $ownId;
314
315                 if (isset($myConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $myConstraints[$idGroup])) {
316                 
317                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
318                         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the own side! ' . print_r($cc, 1));
319                     }
320                     throw new Tinebase_Exception_InvalidRelationConstraints();
321                 }
322                 
323                 // TODO: if the other side gets the config reverted here, validating constrains failes here on multiple update 
324                 foreach($relatedIds as $relatedId) {
325                     $idGroup = $group . '--' . $relatedId;
326                     
327                     if (isset($allConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $allConstraints[$idGroup])) {
328                         
329                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
330                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the other side! ' . print_r($cc, 1));
331                         }
332
333                         throw new Tinebase_Exception_InvalidRelationConstraints();
334                     }
335                 }
336             }
337         }
338     }
339     
340     /**
341      * get all relations of a given record
342      * - cache result if caching is activated
343      * 
344      * @param  string       $_model         own model to get relations for
345      * @param  string       $_backend       own backend to get relations for
346      * @param  string|array $_id            own id to get relations for
347      * @param  string       $_degree        only return relations of given degree
348      * @param  array        $_type          only return relations of given type
349      * @param  bool         $_ignoreACL     get relations without checking permissions
350      * @param  array        $_relatedModels only return relations having this related models
351      * @return Tinebase_Record_RecordSet of Tinebase_Model_Relation
352      */
353     public function getRelations($_model, $_backend, $_id, $_degree = NULL, array $_type = array(), $_ignoreACL = FALSE, $_relatedModels = NULL)
354     {
355         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . "  model: '$_model' backend: '$_backend' " 
356             // . 'ids: ' . print_r((array)$_id, true)
357         );
358         
359         $result = $this->_backend->getAllRelations($_model, $_backend, $_id, $_degree, $_type, FALSE, $_relatedModels);
360         $this->resolveAppRecords($result, $_ignoreACL);
361         
362         return $result;
363     }
364     
365     /**
366      * get all relations of all given records
367      * 
368      * @param  string $_model         own model to get relations for
369      * @param  string $_backend       own backend to get relations for
370      * @param  array  $_ids           own ids to get relations for
371      * @param  string $_degree        only return relations of given degree
372      * @param  array  $_type          only return relations of given type
373      * @param  bool   $_ignoreACL     get relations without checking permissions
374      * @param  array  $_relatedModels only return relations having this related model
375      * @return array  key from $_ids => Tinebase_Record_RecordSet of Tinebase_Model_Relation
376      */
377     public function getMultipleRelations($_model, $_backend, $_ids, $_degree = NULL, array $_type = array(), $_ignoreACL = FALSE, $_relatedModels = NULL)
378     {
379         // prepare a record set for each given id
380         $result = array();
381         foreach ($_ids as $key => $id) {
382             $result[$key] = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', array(),  true);
383         }
384         
385         // fetch all relations in a single set
386         $relations = $this->getRelations($_model, $_backend, $_ids, $_degree, $_type, $_ignoreACL, $_relatedModels);
387         
388         // sort relations into corrensponding sets
389         foreach ($relations as $relation) {
390             $keys = array_keys($_ids, $relation->own_id);
391             foreach ($keys as $key) {
392                 $result[$key]->addRecord($relation);
393             }
394         }
395         
396         return $result;
397     }
398     
399     /**
400      * converts related_records into their appropriate record objects
401      * @todo move to model->setFromJson
402      * 
403      * @param  Tinebase_Model_Relation|Tinebase_Record_RecordSet
404      * @return void
405      */
406     protected function _relatedRecordToObject($_relations)
407     {
408         if(! $_relations instanceof Tinebase_Record_RecordSet) {
409             $_relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation', array($_relations));
410         }
411         
412         foreach ($_relations as $relation) {
413             if (! is_string($relation->related_model)) {
414                 throw new Tinebase_Exception_InvalidArgument('missing relation model');
415             }
416
417             if (empty($relation->related_record) || $relation->related_record instanceof $relation->related_model) {
418                 continue;
419             }
420             
421             $data = Zend_Json::encode($relation->related_record);
422             $relation->related_record = new $relation->related_model();
423             $relation->related_record->setFromJsonInUsersTimezone($data);
424         }
425     }
426     
427     /**
428      * creates/updates application records
429      * 
430      * @param   Tinebase_Record_RecordSet of Tinebase_Model_Relation
431      * @param   bool $_doCreateUpdateCheck
432      * @throws  Tinebase_Exception_UnexpectedValue
433      */
434     protected function _setAppRecord($_relation, $_doCreateUpdateCheck = false)
435     {
436         if (! $_relation->related_record instanceof Tinebase_Record_Abstract) {
437             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
438                 . ' Relation: ' . print_r($_relation->toArray(), TRUE));
439             throw new Tinebase_Exception_UnexpectedValue('Related record is missing from relation.');
440         }
441         
442         $appController = Tinebase_Core::getApplicationInstance($_relation->related_model);
443         
444         if (! $_relation->related_record->getId()) {
445             $method = 'create';
446         } else {
447             $method = 'update';
448         }
449
450         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
451             . ' ' . ucfirst($method) . ' ' . $_relation->related_model . ' record.');
452         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
453             . ' Relation: ' . print_r($_relation->toArray(), TRUE));
454
455         $record = $appController->$method($_relation->related_record, $_doCreateUpdateCheck && $this->_doCreateUpdateCheck($_relation));
456         $_relation->related_id = $record->getId();
457         
458         switch ($_relation->related_model) {
459             case 'Addressbook_Model_Contact':
460                 $_relation->related_backend = ucfirst(Addressbook_Backend_Factory::SQL);
461                 break;
462             case 'Tasks_Model_Task':
463                 $_relation->related_backend = Tasks_Backend_Factory::SQL;
464                 break;
465             default:
466                 $_relation->related_backend = Tinebase_Model_Relation::DEFAULT_RECORD_BACKEND;
467                 break;
468         }
469     }
470
471     /**
472      * get configuration for duplicate/freebusy checks from relatable config
473      *
474      * @param $relation
475      *
476      * TODO relatable config should be an object with functions to get the needed information...
477      */
478     protected function _doCreateUpdateCheck($relation)
479     {
480         $relatableConfig = call_user_func($relation->own_model . '::getRelatableConfig');
481         foreach ($relatableConfig as $config) {
482             if ($relation->related_model === $config['relatedApp'] . '_Model_' . $config['relatedModel']
483                 && isset($config['createUpdateCheck'])
484             ) {
485                 return $config['createUpdateCheck'];
486             }
487         }
488         return false;
489     }
490     
491     /**
492      * resolved app records and fills the related_record property with the corresponding record
493      * 
494      * NOTE: With this, READ ACL is implicitly checked as non readable records won't get retuned!
495      * 
496      * @param  Tinebase_Record_RecordSet $_relations of Tinebase_Model_Relation
497      * @param  boolean $_ignoreACL 
498      * @return void
499      * 
500      * @todo    make getApplicationInstance work for tinebase record (Tinebase_Model_User for example)
501      */
502     protected function resolveAppRecords($_relations, $_ignoreACL = FALSE)
503     {
504         // separate relations by model
505         $modelMap = array();
506         foreach ($_relations as $relation) {
507             if (!(isset($modelMap[$relation->related_model]) || array_key_exists($relation->related_model, $modelMap))) {
508                 $modelMap[$relation->related_model] = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
509             }
510             $modelMap[$relation->related_model]->addRecord($relation);
511         }
512         
513         // fill related_record
514         foreach ($modelMap as $modelName => $relations) {
515             
516             // check right
517             $split = explode('_Model_', $modelName);
518             $rightClass = $split[0] . '_Acl_Rights';
519             $rightName = 'manage_' . strtolower($split[1]) . 's';
520             
521             if (class_exists($rightClass)) {
522                 
523                 $ref = new ReflectionClass($rightClass);
524                 $u = Tinebase_Core::getUser();
525                 
526                 // if a manage right is defined and the user has no manage_record or admin right, remove relations having this record class as related model
527                 if (is_object($u) && $ref->hasConstant(strtoupper($rightName)) && (! $u->hasRight($split[0], $rightName)) && (! $u->hasRight($split[0], Tinebase_Acl_Rights::ADMIN))) {
528                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
529                         $_relations->removeRecords($relations);
530                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Skipping relation due to no manage right: ' . $modelName);
531                     }
532                     continue;
533                 }
534             }
535             
536             $getMultipleMethod = 'getMultiple';
537             
538             if ($modelName === 'Tinebase_Model_User') {
539                 // @todo add related backend here
540                 //$appController = Tinebase_User::factory($relations->related_backend);
541
542                 $appController = Tinebase_User::factory(Tinebase_User::getConfiguredBackend());
543                 $records = $appController->$getMultipleMethod($relations->related_id);
544             } else {
545                 try {
546                     $appController = Tinebase_Core::getApplicationInstance($modelName);
547                     if (method_exists($appController, $getMultipleMethod)) {
548                         $records = $appController->$getMultipleMethod($relations->related_id, $_ignoreACL);
549                         
550                         // resolve record alarms
551                         if (count($records) > 0 && $records->getFirstRecord()->has('alarms')) {
552                             $appController->getAlarms($records);
553                         }
554                     } else {
555                         throw new Tinebase_Exception_AccessDenied('Controller ' . get_class($appController) . ' has no method ' . $getMultipleMethod);
556                     }
557                 } catch (Tinebase_Exception_AccessDenied $tea) {
558                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
559                         . ' Removing relations from result. Got exception: ' . $tea->getMessage());
560                     $_relations->removeRecords($relations);
561                     continue;
562                 }
563             }
564
565             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
566                 " Resolving " . count($relations) . " relations");
567
568             foreach ($relations as $relation) {
569                 $recordIndex    = $records->getIndexById($relation->related_id);
570                 $relationIndex  = $_relations->getIndexById($relation->getId());
571                 if ($recordIndex !== false) {
572                     $_relations[$relationIndex]->related_record = $records[$recordIndex];
573                 } else {
574                     // delete relation from set, as READ ACL is obviously not granted 
575                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
576                         " removing $relation->related_model $relation->related_backend $relation->related_id (ACL)");
577                     unset($_relations[$relationIndex]);
578                 }
579             }
580         }
581     }
582     
583     /**
584      * get list of relations
585      *
586      * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
587      * @param Tinebase_Model_Pagination|optional $_pagination
588      * @param boolean $_onlyIds
589      * @return Tinebase_Record_RecordSet|array
590      */
591     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_onlyIds = FALSE)
592     {
593         return $this->_backend->search($_filter, $_pagination, $_onlyIds);
594     }
595     
596     /**
597      * adds a new relation
598      * 
599      * @param   Tinebase_Model_Relation $_relation 
600      * @return  Tinebase_Model_Relation|NULL the new relation
601      * @throws  Tinebase_Exception_Record_Validation
602      */
603     protected function _addRelation(Tinebase_Model_Relation $_relation)
604     {
605         $_relation->created_by = Tinebase_Core::getUser()->getId();
606         $_relation->creation_time = Tinebase_DateTime::now();
607         if (!$_relation->isValid()) {
608             throw new Tinebase_Exception_Record_Validation('Relation is not valid' . print_r($_relation->getValidationErrors(),true));
609         }
610         
611         try {
612             $result = $this->_backend->addRelation($_relation);
613         } catch(Zend_Db_Statement_Exception $zse) {
614             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not add relation: ' . $zse->getMessage());
615             $result = NULL;
616         }
617         
618         return $result;
619     }
620     
621     /**
622      * update an existing relation
623      * 
624      * @param  Tinebase_Model_Relation $_relation 
625      * @return Tinebase_Model_Relation the updated relation
626      */
627     protected function _updateRelation($_relation)
628     {
629         $_relation->last_modified_by = Tinebase_Core::getUser()->getId();
630         $_relation->last_modified_time = Tinebase_DateTime::now();
631         
632         return $this->_backend->updateRelation($_relation);
633     }
634     
635     /**
636      * replaces all relations to or from a record with $sourceId to a record with $destinationId
637      * 
638      * @param string $sourceId
639      * @param string $destinationId
640      * @param string $model
641      * 
642      * @return array
643      */
644     public function transferRelations($sourceId, $destinationId, $model)
645     {
646         if (! Tinebase_Core::getUser()->hasRight('Tinebase', Tinebase_Acl_Rights::ADMIN)) {
647             throw new Tinebase_Exception_AccessDenied('Non admins of Tinebase aren\'t allowed to perform his operation!');
648         }
649         
650         return $this->_backend->transferRelations($sourceId, $destinationId, $model);
651     }
652
653     /**
654      * Deletes entries
655      *
656      * @param string|integer|Tinebase_Record_Interface|array $_id
657      * @return void
658      * @return int The number of affected rows.
659      */
660     public function delete($_id)
661     {
662         return $this->_backend->delete($_id);
663     }
664
665     /**
666      * remove all relations for application
667      *
668      * @param string $applicationName
669      *
670      * @return void
671      */
672     public function removeApplication($applicationName)
673     {
674         $this->_backend->removeApplication($applicationName);
675     }
676 }