Merge branch '2013.03'
[tine20] / tine20 / Tinebase / Record / Abstract.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  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  */
11
12 /**
13  * Abstract implemetation of Tinebase_Record_Interface
14  * 
15  * @package     Tinebase
16  * @subpackage  Record
17  */
18 abstract class Tinebase_Record_Abstract implements Tinebase_Record_Interface
19 {
20     /**
21      * ISO8601LONG datetime representation
22      */
23     const ISO8601LONG = 'Y-m-d H:i:s';
24     
25     /**
26      * holds the configuration object (must be declared in the concrete class)
27      *
28      * @var Tinebase_ModelConfiguration
29      */
30     protected static $_configurationObject = NULL;
31
32     /**
33      * Holds the model configuration (must be assigned in the concrete class)
34      * 
35      * @var array
36      */
37     protected static $_modelConfiguration = NULL;
38
39     /**
40      * should datas be validated on the fly(false) or only on demand(true)
41      *
42      * @var bool
43      */
44     public $bypassFilters;
45     
46     /**
47      * should datetimeFields be converted from iso8601 (or optionally others {@see $this->dateConversionFormat}) strings to DateTime objects and back 
48      *
49      * @var bool
50      */
51     public $convertDates;
52     
53     /**
54      * differnet format than iso8601 to use for conversions 
55      *
56      * @var string
57      */
58     public $dateConversionFormat = NULL;
59     
60     /**
61      * key in $_validators/$_properties array for the field which 
62      * represents the identifier
63      * NOTE: _Must_ be set by the derived classes!
64      * 
65      * @var string
66      */
67     protected $_identifier = NULL;
68     
69     /**
70      * application the record belongs to
71      * NOTE: _Must_ be set by the derived classes!
72      *
73      * @var string
74      */
75     protected $_application = NULL;
76     
77     /**
78      * stores if values got modified after loaded via constructor
79      * 
80      * @var bool
81      */
82     protected $_isDirty = false;
83     
84     /**
85      * holds properties of record
86      * 
87      * @var array 
88      */
89     protected $_properties = array();
90     
91     /**
92      * this filter get used when validating user generated content with Zend_Input_Filter
93      *
94      * @var array list of zend inputfilter
95      */
96     protected $_filters = array();
97     
98     /**
99      * Defintion of properties. All properties of record _must_ be declared here!
100      * This validators get used when validating user generated content with Zend_Input_Filter
101      * NOTE: _Must_ be set by the derived classes!
102      * 
103      * @var array list of zend validator
104      */
105     protected $_validators = array();
106     
107     /**
108      * the validators place there validation errors in this variable
109      * 
110      * @var array list of validation errors
111      */
112     protected $_validationErrors = array();
113     
114     /**
115      * name of fields containing datetime or an array of datetime
116      * information
117      *
118      * @var array list of datetime fields
119      */
120     protected $_datetimeFields = array();
121     
122     /**
123      * alarm datetime field
124      *
125      * @var string
126      */
127     protected $_alarmDateTimeField = '';
128     
129     /**
130      * name of fields containing time information
131      *
132      * @var array list of time fields
133      */
134     protected $_timeFields = array();
135
136     /**
137      * name of fields that should be omited from modlog
138      *
139      * @var array list of modlog omit fields
140      */
141     protected $_modlogOmitFields = array();
142     
143     /**
144      * name of fields that should not be persisted during create/update in backend
145      *
146      * @var array
147      * 
148      * @todo think about checking the setting of readonly field and not allow it
149      */
150     protected $_readOnlyFields = array();
151     
152     /**
153      * save state if data are validated
154      *
155      * @var bool
156      */
157     protected $_isValidated = false;
158     
159     /**
160      * fields to translate when translate() function is called
161      *
162      * @var array
163      */
164     protected $_toTranslate = array();
165     
166     /**
167      * holds instance of Zend_Filters
168      *
169      * @var array
170      */
171     protected static $_inputFilters = array();
172     
173     /**
174      * If model is relatable and a special config should be applied, this is configured here
175      * @var array
176      */
177     protected static $_relatableConfig = NULL;
178
179     /**
180      * if foreign Id fields should be resolved on search and get from json
181      * should have this format: 
182      *     array('Calendar_Model_Contact' => 'contact_id', ...)
183      * or for more fields:
184      *     array('Calendar_Model_Contact' => array('contact_id', 'customer_id), ...)
185      * (e.g. resolves contact_id with the corresponding Model)
186      * 
187      * @var array
188      */
189     protected static $_resolveForeignIdFields = NULL;
190     
191     /**
192      * this property holds all field information for autoboot strapping
193      * if this is not null, these properties will be overridden in the abstract constructor:
194      *     - _filters
195      *     - _validators
196      *     - _dateTimeFields
197      *     - _alarmDateTimeField
198      *     - _timeFields
199      *     - _modlogOmitFields
200      *     - _readOnlyFields
201      *     - _resolveForeignIdFields
202      * @var array
203      */
204     protected static $_fields = NULL;
205     
206     /**
207      * right, user must have to see the module for this model
208      */
209     protected static $_requiredRight = NULL;
210     
211     /******************************** functions ****************************************/
212     
213     /**
214      * Default constructor
215      * Constructs an object and sets its record related properties.
216      * 
217      * @todo what happens if not all properties in the datas are set?
218      * The default values must also be set, even if no filtering is done!
219      * 
220      * @param mixed $_data
221      * @param bool $bypassFilters sets {@see this->bypassFilters}
222      * @param mixed $convertDates sets {@see $this->convertDates} and optionaly {@see $this->$dateConversionFormat}
223      * @return void
224      * @throws Tinebase_Exception_Record_DefinitionFailure
225      */
226     public function __construct($_data = NULL, $_bypassFilters = false, $_convertDates = true)
227     {
228         // apply configuration
229         $this->_setFromConfigurationObject();
230         
231         if ($this->_identifier === NULL) {
232             throw new Tinebase_Exception_Record_DefinitionFailure('$_identifier is not declared');
233         }
234         
235         $this->bypassFilters = (bool)$_bypassFilters;
236         $this->convertDates = (bool)$_convertDates;
237         if (is_string($_convertDates)) {
238             $this->dateConversionFormat = $_convertDates;
239         }
240
241         if ($this->has('description') && (! array_key_exists('description', $this->_filters))) {
242             $this->_filters['description'] = new Tinebase_Model_InputFilter_CrlfConvert();
243         }
244
245         if (is_array($_data)) {
246             $this->setFromArray($_data);
247         }
248         
249         $this->_isDirty = false;
250     }
251     
252     /**
253      * returns the configuration object
254      *
255      * @return Tinebase_ModelConfiguration|NULL
256      */
257     public static function getConfiguration()
258     {
259         if (! isset (static::$_modelConfiguration)) {
260             return NULL;
261         }
262         
263         if (! static::$_configurationObject) {
264             static::$_configurationObject = new Tinebase_ModelConfiguration(static::$_modelConfiguration);
265         }
266     
267         return static::$_configurationObject;
268     }
269     
270     /**
271      * returns the relation config
272      * 
273      * @deprecated
274      * @return array
275      */
276     public static function getRelatableConfig()
277     {
278         return static::$_relatableConfig;
279     }
280     
281     /**
282      * recursivly clone properties
283      */
284     public function __clone()
285     {
286         foreach ($this->_properties as $name => $value)
287         {
288             if (is_object($value)) {
289                 $this->_properties[$name] = clone $value;
290             } else if (is_array($value)) {
291                 foreach ($value as $arrKey => $arrValue) {
292                     if (is_object($arrValue)) {
293                         $value[$arrKey] = clone $arrValue;
294                     }
295                 }
296             }
297         }
298     }
299     
300     /**
301      * sets identifier of record
302      * 
303      * @param int identifier
304      * @return void
305      */
306     public function setId($_id)
307     {
308         // set internal state to "not validated"
309         $this->_isValidated = false;
310         
311         if ($this->bypassFilters === true) {
312             $this->_properties[$this->_identifier] = $_id;
313         } else {
314             $this->__set($this->_identifier, $_id);
315         }
316     }
317     
318     /**
319      * gets identifier of record
320      * 
321      * @return int identifier
322      */
323     public function getId()
324     {
325         if (! isset($this->_properties[$this->_identifier])) {
326             $this->setId(NULL);
327         }
328         return $this->_properties[$this->_identifier];
329     }
330     
331     /**
332      * gets application the records belongs to
333      * 
334      * @return string application
335      */
336     public function getApplication()
337     {
338         return $this->_application;
339     }
340     
341     /**
342      * returns id property of this model
343      *
344      * @return string
345      */
346     public function getIdProperty()
347     {
348         return $this->_identifier;
349     }
350     
351     /**
352      * sets the record related properties from user generated input.
353      * 
354      * Input-filtering and validation by Zend_Filter_Input can enabled and disabled
355      *
356      * @param array $_data            the new data to set
357      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
358      * 
359      * @todo remove custom fields handling (use Tinebase_Record_RecordSet for them)
360      */
361     public function setFromArray(array $_data)
362     {
363         if ($this->convertDates === true) {
364             if (! is_string($this->dateConversionFormat)) {
365                 $this->_convertISO8601ToDateTime($_data);
366             } else {
367                 $this->_convertCustomDateToDateTime($_data, $this->dateConversionFormat);
368             }
369             
370             $this->_convertTime($_data);
371         }
372         
373         // set internal state to "not validated"
374         $this->_isValidated = false;
375
376         // get custom fields
377         if ($this->has('customfields')) {
378             $application = Tinebase_Application::getInstance()->getApplicationByName($this->_application);
379             $customFields = Tinebase_CustomField::getInstance()->getCustomFieldsForApplication($application, get_class($this))->name;
380             $recordCustomFields = array();
381         } else {
382             $customFields = array();
383         }
384         
385         // make sure we run through the setters
386         $bypassFilter = $this->bypassFilters;
387         $this->bypassFilters = true;
388         foreach ($_data as $key => $value) {
389             if (array_key_exists ($key, $this->_validators)) {
390                 $this->$key = $value;
391             } else if (in_array($key, $customFields)) {
392                 $recordCustomFields[$key] = $value;
393             }
394         }
395         if (!empty($recordCustomFields)) {
396             $this->customfields = $recordCustomFields;
397         }
398         
399         $this->bypassFilters = $bypassFilter;
400         if ($this->bypassFilters !== true) {
401             $this->isValid(true);
402         }
403     }
404     
405     /**
406      * wrapper for setFromJason which expects datetimes in array to be in
407      * users timezone and converts them to UTC
408      *
409      * @todo move this to a generic __call interceptor setFrom<API>InUsersTimezone
410      * 
411      * @param  string $_data json encoded data
412      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
413      */
414     public function setFromJsonInUsersTimezone($_data)
415     {
416         // change timezone of current php process to usertimezone to let new dates be in the users timezone
417         // NOTE: this is neccessary as creating the dates in UTC and just adding/substracting the timeshift would
418         //       lead to incorrect results on DST transistions 
419         date_default_timezone_set(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
420
421         // NOTE: setFromArray creates new Tinebase_DateTimes of $this->datetimeFields
422         $this->setFromJson($_data);
423         
424         // convert $this->_datetimeFields into the configured server's timezone (UTC)
425         $this->setTimezone('UTC');
426         
427         // finally reset timzone of current php process to the configured server timezone (UTC)
428         date_default_timezone_set('UTC');
429     }
430     
431     /**
432      * Sets timezone of $this->_datetimeFields
433      * 
434      * @see Tinebase_DateTime::setTimezone()
435      * @param  string $_timezone
436      * @param  bool   $_recursive
437      * @return  void
438      * @throws Tinebase_Exception_Record_Validation
439      */
440     public function setTimezone($_timezone, $_recursive = TRUE)
441     {
442         foreach ($this->_datetimeFields as $field) {
443             if (!isset($this->_properties[$field])) continue;
444             
445             if (!is_array($this->_properties[$field])) {
446                 $toConvert = array($this->_properties[$field]);
447             } else {
448                 $toConvert = $this->_properties[$field];
449             }
450
451             
452             foreach ($toConvert as $field => &$value) {
453                 
454                 if (! method_exists($value, 'setTimezone')) {
455                     throw new Tinebase_Exception_Record_Validation($field . 'must be a method setTimezone');
456                 } 
457                 $value->setTimezone($_timezone);
458             } 
459         }
460         
461         if ($_recursive) {
462             foreach ($this->_properties as $property => $value) {
463                 if (is_object($value) && 
464                         (in_array('Tinebase_Record_Interface', class_implements($value)) || 
465                         $value instanceof Tinebase_Record_Recordset) ) {
466                        
467                     $value->setTimezone($_timezone, TRUE);
468                 }
469             }
470         }
471     }
472     
473     /**
474      * returns array of fields with validation errors 
475      *
476      * @return array
477      */
478     public function getValidationErrors()
479     {
480         return $this->_validationErrors;
481     }
482     
483     /**
484      * returns array with record related properties 
485      *
486      * @param boolean $_recursive
487      * @return array
488      */
489     public function toArray($_recursive = TRUE)
490     {
491         /*
492         foreach ($this->_properties as $key => $value) {
493             if ($value instanceof DateTime) {
494                 $date = new Tinebase_DateTime($value->get(Tinebase_Record_Abstract::ISO8601LONG));
495                 $date->setTimezone($value->getTimezone());
496                 $this->_properties[$key] = $date;
497             }
498         }
499         */
500         $recordArray = $this->_properties;
501         if ($this->convertDates === true) {
502             if (! is_string($this->dateConversionFormat)) {
503                 $this->_convertDateTimeToString($recordArray, Tinebase_Record_Abstract::ISO8601LONG);
504             } else {
505                 $this->_convertDateTimeToString($recordArray, $this->dateConversionFormat);
506             }
507         }
508         
509         if ($_recursive) {
510             foreach ($recordArray as $property => $value) {
511                 if ($this->_hasToArray($value)) {
512                     $recordArray[$property] = $value->toArray();
513                 }
514             }
515         }
516         
517         return $recordArray;
518     }
519     
520     /**
521      * checks if variable has toArray()
522      * 
523      * @param mixed $mixed
524      * @return boolean
525      */
526     protected function _hasToArray($mixed)
527     {
528         return (is_object($mixed) && 
529                         (in_array('Tinebase_Record_Interface', class_implements($mixed)) || 
530                         $mixed instanceof Tinebase_Record_Recordset) ||
531                         (is_object($mixed) && method_exists($mixed, 'toArray')));
532     }
533     
534     /**
535      * validate and filter the the internal data
536      *
537      * @param $_throwExceptionOnInvalidData
538      * @return bool
539      * @throws Tinebase_Exception_Record_Validation
540      */
541     public function isValid($_throwExceptionOnInvalidData = false)
542     {
543         if ($this->_isValidated === false) {
544             
545             $inputFilter = $this->_getFilter();
546             $inputFilter->setData($this->_properties);
547             
548             if ($inputFilter->isValid()) {
549                 // set $this->_properties with the filtered values
550                 $this->_properties = $inputFilter->getUnescaped();
551                 $this->_isValidated = true;
552                 
553             } else {
554                 $this->_validationErrors = array();
555                 
556                 foreach($inputFilter->getMessages() as $fieldName => $errorMessage) {
557                     //print_r($inputFilter->getMessages());
558                     $this->_validationErrors[] = array(
559                         'id'  => $fieldName,
560                         'msg' => $errorMessage
561                     );
562                 }
563                 if ($_throwExceptionOnInvalidData) {
564                     $e = new Tinebase_Exception_Record_Validation('some fields ' . implode(',', array_keys($inputFilter->getMessages())) . ' have invalid content');
565                     Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ":\n" .
566                         print_r($this->_validationErrors,true). $e);
567                     throw $e;
568                 }
569             }
570         }
571         
572         return $this->_isValidated;
573     }
574     
575     /**
576      * apply filter
577      *
578      * @todo implement
579      */
580     public function applyFilter()
581     {
582         $this->isValid(true);
583         
584     }
585     
586     /**
587      * sets record related properties
588      * 
589      * @param string _name of property
590      * @param mixed _value of property
591      * @throws Tinebase_Exception_UnexpectedValue
592      * @return void
593      */
594     public function __set($_name, $_value)
595     {
596         if (! array_key_exists ($_name, $this->_validators)) {
597             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
598         }
599         
600         $this->_properties[$_name] = $_value;
601         $this->_isValidated = false;
602         $this->_isDirty     = true;
603         
604         if ($this->bypassFilters !== true) {
605             $this->isValid(true);
606         }
607     }
608     
609     /**
610      * unsets record related properties
611      * 
612      * @param string _name of property
613      * @throws Tinebase_Exception_UnexpectedValue
614      * @return void
615      */
616     public function __unset($_name)
617     {
618         if (!array_key_exists ($_name, $this->_validators)) {
619             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
620         }
621         
622         unset($this->_properties[$_name]);
623         
624         $this->_isValidated = false;
625         
626         if ($this->bypassFilters !== true) {
627             $this->isValid(true);
628         }
629     }
630     
631     /**
632      * checkes if an propertiy is set
633      * 
634      * @param string _name name of property
635      * @return bool property is set or not
636      */
637     public function __isset($_name)
638     {
639         return isset($this->_properties[$_name]);
640     }
641     
642     /**
643      * gets record related properties
644      * 
645      * @param string _name of property
646      * @throws Tinebase_Exception_UnexpectedValue
647      * @return mixed value of property
648      */
649     public function __get($_name)
650     {
651         if (!array_key_exists ($_name, $this->_validators)) {
652             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
653         }
654         
655         return array_key_exists($_name, $this->_properties) ? $this->_properties[$_name] : NULL;
656     }
657     
658    /** convert this to string
659     *
660     * @return string
661     */
662     public function __toString()
663     {
664        return print_r($this->toArray(), TRUE);
665     }
666     
667     /**
668      * returns a Zend_Filter for the $_filters and $_validators of this record class.
669      * we just create an instance of Filter if we really need it.
670      * 
671      * @return Zend_Filter_Input
672      */
673     protected function _getFilter()
674     {
675         $myClassName = get_class($this);
676         
677         if (! array_key_exists($myClassName, self::$_inputFilters)) {
678             self::$_inputFilters[$myClassName] = new Zend_Filter_Input($this->_filters, $this->_validators);
679         }
680         return self::$_inputFilters[$myClassName];
681     }
682     
683     /**
684      * Converts Tinebase_DateTimes into custom representation
685      *
686      * @param array &$_toConvert
687      * @param string $_format
688      * @return void
689      */
690     protected function _convertDateTimeToString(&$_toConvert, $_format)
691     {
692         //$_format = "Y-m-d H:i:s";
693         foreach ($_toConvert as $field => &$value) {
694             if ($value instanceof DateTime) {
695                 $_toConvert[$field] = $value->format($_format);
696             } elseif (is_array($value)) {
697                 $this->_convertDateTimeToString($value, $_format);
698             } elseif (! $value && in_array($field, $this->_datetimeFields)) {
699                 $_toConvert[$field] = NULL;
700             }
701         }
702     }
703     
704     /**
705      * Converts iso8601 formated dates into Tinebase_DateTime representation
706      * 
707      * NOTE: Instead of using the Tinebase_DateTime build in date creation from iso, we 
708      *       first convert the dates to UNIX timestamp by hand and create Tinebase_DateTimes
709      *       from this timestamp. This brings a 15 times performance boost
710      *
711      * @param array &$_data
712      * 
713      * @return void
714      */
715     protected function _convertISO8601ToDateTime(array &$_data)
716     {
717         foreach ($this->_datetimeFields as $field) {
718             if (!isset($_data[$field]) || $_data[$field] instanceof DateTime) continue;
719             
720             if (! is_array($_data[$field]) && strpos($_data[$field], ',') !== false) {
721                 $_data[$field] = explode(',', $_data[$field]);
722             }
723             
724             try {
725                 if (is_array($_data[$field])) {
726                     foreach($_data[$field] as $dataKey => $dataValue) {
727                         if ($dataValue instanceof DateTime) continue;
728                         $_data[$field][$dataKey] =  (int)$dataValue == 0 ? NULL : new Tinebase_DateTime($dataValue);
729                     }
730                 } else {
731                     $_data[$field] = (int)$_data[$field] == 0 ? NULL : new Tinebase_DateTime($_data[$field]);
732                     
733                 }
734             } catch (Tinebase_DateTime_Exception $zde) {
735                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Error while converting date field "' . $field . '": ' . $zde->getMessage());
736                 $_data[$field] = NULL;
737             }
738         }
739         
740     }
741     
742     /**
743      * Converts custom formated dates into Tinebase_DateTime representation
744      *
745      * @param array &$_data
746      * @param string $_format {@see Tinebase_DateTime}
747      * 
748      * @return void
749      */
750     protected function _convertCustomDateToDateTime(array &$_data, $_format)
751     {
752         foreach ($this->_datetimeFields as $field) {
753             if (!isset($_data[$field]) || $_data[$field] instanceof DateTime) continue;
754             
755             if (strpos($_data[$field], ',') !== false) {
756                 $_data[$field] = explode(',', $_data[$field]);
757             }
758             
759             if (is_array($_data[$field])) {
760                 foreach($_data[$field] as $dataKey => $dataValue) {
761                     if ($dataValue instanceof DateTime) continue;
762                     $_data[$field][$dataKey] =  (int)$dataValue == 0 ? NULL : new Tinebase_DateTime($dataValue);
763                 }
764             } else {
765                 $_data[$field] = (int)$_data[$field] == 0 ? NULL : new Tinebase_DateTime($_data[$field]);
766             }
767         }
768     }
769     
770     /**
771      * cut the timezone-offset from the iso representation in order to force 
772      * Tinebase_DateTime to create dates in the user timezone. otherwise they will be 
773      * created with Etc/GMT+<offset> as timezone which would lead to incorrect 
774      * results in datetime computations!
775      * 
776      * @param  string Tinebase_DateTime::ISO8601 representation of a datetime filed
777      * @return string ISO8601LONG representation ('Y-m-d H:i:s')
778      */
779     protected function _convertISO8601ToISO8601LONG($_ISO)
780     {
781         $cutedISO = preg_replace('/[+\-]{1}\d{2}:\d{2}/', '', $_ISO);
782         $cutedISO = str_replace('T', ' ', $cutedISO);
783         
784         return $cutedISO;
785     }
786     
787     /**
788      * Converts time into iso representation (hh:mm:ss)
789      *
790      * @param array &$_data
791      * @return void
792      * 
793      * @todo    add support for hh:mm:ss AM|PM
794      */
795     protected function _convertTime(&$_data)
796     {
797         foreach ($this->_timeFields as $field) {
798             if (!isset($_data[$field]) || empty($_data[$field])) {
799                 continue;
800             }
801             
802             $hhmmss = explode(":", $_data[$field]);
803             if (count($hhmmss) === 2) {
804                 // seconds missing
805                 $hhmmss[] = '00';
806             }
807             list($hours, $minutes, $seconds) = $hhmmss;
808             if (preg_match('/AM|PM/', $minutes)) {
809                 list($minutes, $notation) = explode(" ", $minutes);
810                 switch($notation) {
811                     case 'AM':
812                         $hours = ($hours == '12') ? 0 : $hours;
813                         break;
814                     case 'PM':
815                         $hours = $hours + 12;
816                         break;
817                 }
818                 $_data[$field] = implode(':', $hhmmss);
819             }
820         }
821     }
822     
823     /**
824      * returns the default filter group for this model
825      * @return string
826      */
827     protected static function _getDefaultFilterGroup()
828     {
829         return get_called_class() . 'Filter';
830     }
831     
832     /**
833      * required by ArrayAccess interface
834      */
835     public function offsetExists($_offset)
836     {
837         return isset($this->_properties[$_offset]);
838     }
839     
840     /**
841      * required by ArrayAccess interface
842      */
843     public function offsetGet($_offset)
844     {
845         return $this->__get($_offset);
846     }
847     
848     /**
849      * required by ArrayAccess interface
850      */
851     public function offsetSet($_offset, $_value)
852     {
853         return $this->__set($_offset, $_value);
854     }
855     
856     /**
857      * required by ArrayAccess interface
858      * @throws Tinebase_Exception_Record_NotAllowed
859      */
860     public function offsetUnset($_offset)
861     {
862         throw new Tinebase_Exception_Record_NotAllowed('Unsetting of properties is not allowed');
863     }
864     
865     /**
866      * required by IteratorAggregate interface
867      */
868     public function getIterator()
869     {
870         return new ArrayIterator($this->_properties);
871     }
872     
873     /**
874      * returns a random 40-character hexadecimal number to be used as 
875      * universal identifier (UID)
876      * 
877      * @param int|optional $_length the length of the uid, defaults to 40
878      * @return string 40-character hexadecimal number
879      */
880     public static function generateUID($_length = false)
881     {
882         $uid = sha1(mt_rand(). microtime());
883         
884         if ($_length !== false) {
885             $uid = substr($uid, 0, $_length);
886         }
887         
888         return $uid;
889     }
890     
891     /**
892     * converts a int, string or Tinebase_Record_Interface to a id
893     *
894     * @param int|string|Tinebase_Record_Abstract $_id the id to convert
895     * @param string $_modelName
896     * @return int|string
897     */
898     public static function convertId($_id, $_modelName = 'Tinebase_Record_Abstract')
899     {
900         if ($_id instanceof $_modelName) {
901             if (! $_id->getId()) {
902                 throw new Tinebase_Exception_InvalidArgument('No id set!');
903             }
904             $id = $_id->getId();
905         } elseif (is_array($_id)) {
906             throw new Tinebase_Exception_InvalidArgument('Id can not be an array!');
907         } else {
908             $id = $_id;
909         }
910     
911         if ($id === 0) {
912             throw new Tinebase_Exception_InvalidArgument($_modelName . '.id can not be 0!');
913         }
914     
915         return $id;
916     }
917     
918     /**
919      * returns a Tinebase_Record_Diff record with differences to the given record
920      * 
921      * @param Tinebase_Record_Interface $_record record for comparison
922      * @param array $omitFields omit fields (for example modlog fields)
923      * @return Tinebase_Record_Diff|NULL
924      */
925     public function diff($_record, $omitFields = array())
926     {
927         if (! $_record instanceof Tinebase_Record_Abstract) {
928             return $_record;
929         }
930         
931         $result = new Tinebase_Record_Diff(array(
932             'id'     => $this->getId(),
933             'model'  => get_class($_record),
934         ));
935         $diff = array();
936         foreach (array_keys($this->_validators) as $fieldName) {
937             if (in_array($fieldName, $omitFields)) {
938                 continue;
939             }
940             
941             $ownField = $this->__get($fieldName);
942             $recordField = $_record->$fieldName;
943             
944             if (in_array($fieldName, $this->_datetimeFields)) {
945                 if ($ownField instanceof DateTime
946                     && $recordField instanceof DateTime) {
947                     if ($ownField->compare($recordField) === 0) {
948                         continue;
949                     } else {
950                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
951                             ' datetime for field ' . $fieldName . ' is not equal: '
952                             . $ownField->getIso() . ' != '
953                             . $recordField->getIso()
954                         );
955                     } 
956                 } else if (! $recordField instanceof DateTime && $ownField == $recordField) {
957                     continue;
958                 } 
959             } else if ($fieldName == $this->_identifier && $this->getId() == $_record->getId()) {
960                 continue;
961             } else if ($ownField instanceof Tinebase_Record_Abstract || $ownField instanceof Tinebase_Record_RecordSet) {
962                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
963                     ' Doing subdiff for field ' . $fieldName);
964                 $subdiff = $ownField->diff($recordField);
965                 if (is_object($subdiff) && ! $subdiff->isEmpty()) {
966                     $diff[$fieldName] = $subdiff;
967                 }
968                 continue;
969             } else if ($recordField instanceof Tinebase_Record_Abstract && is_scalar($ownField)) {
970                 // maybe we have the id of the record -> just compare the id
971                 if ($recordField->getId() == $ownField) {
972                     continue;
973                 } else {
974                     $recordField = $recordField->getId();
975                 }
976             } else if ($ownField == $recordField) {
977                 continue;
978             } else if (empty($ownField) && empty($recordField)) {
979                 continue;
980             }
981             
982             $diff[$fieldName] = $recordField;
983         }
984         
985         $result->diff = $diff;
986         return $result;
987     }
988     
989     /**
990      * check if data got modified
991      * 
992      * @return boolean
993      */
994     public function isDirty()
995     {
996         return $this->_isDirty;
997     }
998     
999     /**
1000      * returns TRUE if given record obsoletes this one
1001      * 
1002      * @param  Tinebase_Record_Interface $_record
1003      * @return bool
1004      */
1005     public function isObsoletedBy($_record)
1006     {
1007         if (get_class($_record) !== get_class($this)) {
1008             throw new Tinebase_Exception_InvalidArgument('Records could not be compared');
1009         } else if ($this->getId() && $_record->getId() !== $this->getId()) {
1010             throw new Tinebase_Exception_InvalidArgument('Record id mismatch');
1011         }
1012         
1013         if ($this->has('seq') && $_record->seq != $this->seq) {
1014             return $_record->seq > $this->seq;
1015         }
1016         
1017         return ($this->has('last_modified_time')) ? $_record->last_modified_time > $this->last_modified_time : TRUE;
1018     }
1019     
1020     /**
1021      * check if two records are equal
1022      * 
1023      * @param  Tinebase_Record_Interface $_record record for comparism
1024      * @param  array                     $_toOmit fields to omit
1025      * @return bool
1026      */
1027     public function isEqual($_record, array $_toOmit = array())
1028     {
1029         $diff = $this->diff($_record);
1030         return ($diff) ? $diff->isEmpty($_toOmit) : FALSE;
1031     }
1032     
1033     /**
1034      * translate this records' fields
1035      *
1036      */
1037     public function translate()
1038     {
1039         // get translation object
1040         if (!empty($this->_toTranslate)) {
1041             $translate = Tinebase_Translation::getTranslation($this->_application);
1042             
1043             foreach ($this->_toTranslate as $field) {
1044                 $this->$field = $translate->_($this->$field);
1045             }
1046         }
1047     }
1048
1049     /**
1050      * check if the model has a specific field (container_id for example)
1051      *
1052      * @param string $_field
1053      * @return boolean
1054      */
1055     public function has($_field) 
1056     {
1057         return (array_key_exists ($_field, $this->_validators));
1058     }   
1059
1060     /**
1061      * get fields
1062      * 
1063      * @return array
1064      */
1065     public function getFields()
1066     {
1067         return array_keys($this->_validators);
1068     }
1069     
1070     /**
1071      * fills a record from json data
1072      *
1073      * @param string $_data json encoded data
1074      * @return void
1075      * 
1076      * @todo replace this (and setFromJsonInUsersTimezone) with Tinebase_Convert_Json::toTine20Model
1077      * @todo move custom _setFromJson to (custom) converter
1078      */
1079     public function setFromJson($_data)
1080     {
1081         if (is_array($_data)) {
1082             $recordData = $_data;
1083         } else {
1084             $recordData = Zend_Json::decode($_data);
1085         }
1086         
1087         // sanitize container id if it is an array
1088         if ($this->has('container_id') && isset($recordData['container_id']) && is_array($recordData['container_id']) && isset($recordData['container_id']['id']) ) {
1089             $recordData['container_id'] = $recordData['container_id']['id'];
1090         }
1091         
1092         $this->_setFromJson($recordData);
1093         $this->setFromArray($recordData);
1094     }
1095     
1096     /**
1097      * can be reimplemented by subclasses to modify values during setFromJson
1098      * @param array $_data the json decoded values
1099      * @return void
1100      */
1101     protected function _setFromJson(array &$_data)
1102     {
1103         
1104     }
1105
1106     /**
1107      * returns modlog omit fields
1108      *
1109      * @return array
1110      */
1111     public function getModlogOmitFields()
1112     {
1113         return $this->_modlogOmitFields;
1114     }
1115
1116     /**
1117      * returns read only fields
1118      *
1119      * @return array
1120      */
1121     public function getReadOnlyFields()
1122     {
1123         return $this->_readOnlyFields;
1124     }
1125
1126     /**
1127      * sets the non static properties by the created configuration object on instantiation
1128      */
1129     protected function _setFromConfigurationObject()
1130     {
1131         // set protected, non static properties
1132         $co = static::getConfiguration();
1133         if ($co && $mc = $co->toArray()) {
1134             foreach ($mc as $property => $value) {
1135                 $this->{$property} = $value;
1136             }
1137         }
1138     }
1139
1140     /**
1141      * returns the title of the record
1142      * 
1143      * @return string
1144      */
1145     public function getTitle()
1146     {
1147         $c = static::getConfiguration();
1148         return $this->{($c ? $c->titleProperty : 'title')};
1149     }
1150     
1151     /**
1152      * returns the foreignId fields (used in Tinebase_Convert_Json)
1153      * @return array
1154      */
1155     public static function getResolveForeignIdFields()
1156     {
1157         return static::$_resolveForeignIdFields;
1158     }
1159 }