diff(): date field in record could be DateTime without compare()
[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') && (! (isset($this->_filters['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             $this->_convertISO8601ToDateTime($_data);
365         }
366         
367         // set internal state to "not validated"
368         $this->_isValidated = false;
369
370         // get custom fields
371         if ($this->has('customfields')) {
372             $application = Tinebase_Application::getInstance()->getApplicationByName($this->_application);
373             $customFields = Tinebase_CustomField::getInstance()->getCustomFieldsForApplication($application, get_class($this))->name;
374             $recordCustomFields = array();
375         } else {
376             $customFields = array();
377         }
378         
379         // make sure we run through the setters
380         $bypassFilter = $this->bypassFilters;
381         $this->bypassFilters = true;
382         foreach ($_data as $key => $value) {
383             if ((isset($this->_validators[$key]) || array_key_exists ($key, $this->_validators))) {
384                 $this->$key = $value;
385             } else if (in_array($key, $customFields)) {
386                 $recordCustomFields[$key] = $value;
387             }
388         }
389         if (!empty($recordCustomFields)) {
390             $this->customfields = $recordCustomFields;
391         }
392         
393         $this->bypassFilters = $bypassFilter;
394         if ($this->bypassFilters !== true) {
395             $this->isValid(true);
396         }
397     }
398     
399     /**
400      * wrapper for setFromJason which expects datetimes in array to be in
401      * users timezone and converts them to UTC
402      *
403      * @todo move this to a generic __call interceptor setFrom<API>InUsersTimezone
404      * 
405      * @param  string $_data json encoded data
406      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
407      */
408     public function setFromJsonInUsersTimezone($_data)
409     {
410         // change timezone of current php process to usertimezone to let new dates be in the users timezone
411         // NOTE: this is neccessary as creating the dates in UTC and just adding/substracting the timeshift would
412         //       lead to incorrect results on DST transistions 
413         date_default_timezone_set(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
414
415         // NOTE: setFromArray creates new Tinebase_DateTimes of $this->datetimeFields
416         $this->setFromJson($_data);
417         
418         // convert $this->_datetimeFields into the configured server's timezone (UTC)
419         $this->setTimezone('UTC');
420         
421         // finally reset timzone of current php process to the configured server timezone (UTC)
422         date_default_timezone_set('UTC');
423     }
424     
425     /**
426      * Sets timezone of $this->_datetimeFields
427      * 
428      * @see Tinebase_DateTime::setTimezone()
429      * @param  string $_timezone
430      * @param  bool   $_recursive
431      * @return  void
432      * @throws Tinebase_Exception_Record_Validation
433      */
434     public function setTimezone($_timezone, $_recursive = TRUE)
435     {
436         foreach ($this->_datetimeFields as $field) {
437             if (!isset($this->_properties[$field])) continue;
438             
439             if (!is_array($this->_properties[$field])) {
440                 $toConvert = array($this->_properties[$field]);
441             } else {
442                 $toConvert = $this->_properties[$field];
443             }
444
445             
446             foreach ($toConvert as $field => &$value) {
447                 
448                 if (! method_exists($value, 'setTimezone')) {
449                     throw new Tinebase_Exception_Record_Validation($field . 'must be a method setTimezone');
450                 } 
451                 $value->setTimezone($_timezone);
452             } 
453         }
454         
455         if ($_recursive) {
456             foreach ($this->_properties as $property => $value) {
457                 if ($value && is_object($value) && 
458                         (in_array('Tinebase_Record_Interface', class_implements($value)) || 
459                         $value instanceof Tinebase_Record_Recordset) ) {
460                        
461                     $value->setTimezone($_timezone, TRUE);
462                 }
463             }
464         }
465     }
466     
467     /**
468      * returns array of fields with validation errors 
469      *
470      * @return array
471      */
472     public function getValidationErrors()
473     {
474         return $this->_validationErrors;
475     }
476     
477     /**
478      * returns array with record related properties 
479      *
480      * @param boolean $_recursive
481      * @return array
482      */
483     public function toArray($_recursive = TRUE)
484     {
485         /*
486         foreach ($this->_properties as $key => $value) {
487             if ($value instanceof DateTime) {
488                 $date = new Tinebase_DateTime($value->get(Tinebase_Record_Abstract::ISO8601LONG));
489                 $date->setTimezone($value->getTimezone());
490                 $this->_properties[$key] = $date;
491             }
492         }
493         */
494         $recordArray = $this->_properties;
495         if ($this->convertDates === true) {
496             if (! is_string($this->dateConversionFormat)) {
497                 $this->_convertDateTimeToString($recordArray, Tinebase_Record_Abstract::ISO8601LONG);
498             } else {
499                 $this->_convertDateTimeToString($recordArray, $this->dateConversionFormat);
500             }
501         }
502         
503         if ($_recursive) {
504             foreach ($recordArray as $property => $value) {
505                 if ($this->_hasToArray($value)) {
506                     $recordArray[$property] = $value->toArray();
507                 }
508             }
509         }
510         
511         return $recordArray;
512     }
513     
514     /**
515      * checks if variable has toArray()
516      * 
517      * @param mixed $mixed
518      * @return boolean
519      */
520     protected function _hasToArray($mixed)
521     {
522         return is_object($mixed) && method_exists($mixed, 'toArray');
523     }
524     
525     /**
526      * validate and filter the the internal data
527      *
528      * @param $_throwExceptionOnInvalidData
529      * @return bool
530      * @throws Tinebase_Exception_Record_Validation
531      */
532     public function isValid($_throwExceptionOnInvalidData = false)
533     {
534         if ($this->_isValidated === true) {
535             return true;
536         }
537         
538         $inputFilter = $this->_getFilter()
539             ->setData($this->_properties);
540         
541         if ($inputFilter->isValid()) {
542             // set $this->_properties with the filtered values
543             $this->_properties  = $inputFilter->getUnescaped();
544             $this->_isValidated = true;
545             
546             return true;
547         }
548         
549         $this->_validationErrors = array();
550         
551         foreach ($inputFilter->getMessages() as $fieldName => $errorMessage) {
552             $this->_validationErrors[] = array(
553                 'id'  => $fieldName,
554                 'msg' => $errorMessage
555             );
556         }
557         
558         if ($_throwExceptionOnInvalidData) {
559             $e = new Tinebase_Exception_Record_Validation('Some fields ' . implode(',', array_keys($inputFilter->getMessages()))
560                 . ' have invalid content');
561             
562             if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " "
563                 . $e->getMessage()
564                 . print_r($this->_validationErrors, true));
565             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
566                 . ' Record: ' . print_r($this->toArray(), true));
567             
568             throw $e;
569         }
570         
571         return false;
572     }
573     
574     /**
575      * apply filter
576      *
577      * @todo implement
578      */
579     public function applyFilter()
580     {
581         $this->isValid(true);
582     }
583     
584     /**
585      * sets record related properties
586      * 
587      * @param string _name of property
588      * @param mixed _value of property
589      * @throws Tinebase_Exception_UnexpectedValue
590      * @return void
591      */
592     public function __set($_name, $_value)
593     {
594         if (! (isset($this->_validators[$_name]) || array_key_exists ($_name, $this->_validators))) {
595             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
596         }
597         
598         if ($this->bypassFilters !== true) {
599             $this->_properties[$_name] = $this->_validateField($_name, $_value);
600         } else {
601             $this->_properties[$_name] = $_value;
602             
603             $this->_isValidated = false;
604         }
605         
606         $this->_isDirty = true;
607     }
608     
609     protected function _validateField($name, $value)
610     {
611         $inputFilter = $this->_getFilter($name);
612         $inputFilter->setData(array(
613             $name => $value
614         ));
615         
616         if ($inputFilter->isValid()) {
617             return $inputFilter->getUnescaped($name);
618         }
619         
620         $this->_validationErrors = array();
621         
622         foreach($inputFilter->getMessages() as $fieldName => $errorMessage) {
623             $this->_validationErrors[] = array(
624                 'id'  => $fieldName,
625                 'msg' => $errorMessage
626             );
627         }
628         
629         $e = new Tinebase_Exception_Record_Validation('the field ' . implode(',', array_keys($inputFilter->getMessages())) . ' has invalid content');
630         Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ":\n" .
631             print_r($this->_validationErrors,true). $e);
632         throw $e;
633     }
634     
635     /**
636      * unsets record related properties
637      * 
638      * @param string _name of property
639      * @throws Tinebase_Exception_UnexpectedValue
640      * @return void
641      */
642     public function __unset($_name)
643     {
644         if (!(isset($this->_validators[$_name]) || array_key_exists ($_name, $this->_validators))) {
645             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
646         }
647
648         unset($this->_properties[$_name]);
649         
650         $this->_isValidated = false;
651         
652         if ($this->bypassFilters !== true) {
653             $this->isValid(true);
654         }
655     }
656     
657     /**
658      * checkes if an propertiy is set
659      * 
660      * @param string _name name of property
661      * @return bool property is set or not
662      */
663     public function __isset($_name)
664     {
665         return isset($this->_properties[$_name]);
666     }
667     
668     /**
669      * gets record related properties
670      * 
671      * @param  string  $name  name of property
672      * @return mixed value of property
673      */
674     public function __get($name)
675     {
676         return (isset($this->_properties[$name]) || array_key_exists($name, $this->_properties))
677             ? $this->_properties[$name] 
678             : NULL;
679     }
680     
681    /** convert this to string
682     *
683     * @return string
684     */
685     public function __toString()
686     {
687        return print_r($this->toArray(), TRUE);
688     }
689     
690     /**
691      * returns a Zend_Filter for the $_filters and $_validators of this record class.
692      * we just create an instance of Filter if we really need it.
693      * 
694      * @return Zend_Filter_Input
695      */
696     protected function _getFilter($field = null)
697     {
698         $keyName = get_class($this) . $field;
699         
700         if (! (isset(self::$_inputFilters[$keyName]) || array_key_exists($keyName, self::$_inputFilters))) {
701             if ($field !== null) {
702                 $filters    = (isset($this->_filters[$field]) || array_key_exists($field, $this->_filters)) ? array($field => $this->_filters[$field]) : array();
703                 $validators = array($field => $this->_validators[$field]); 
704                 
705                 self::$_inputFilters[$keyName] = new Zend_Filter_Input($filters, $validators);
706             } else {
707                 self::$_inputFilters[$keyName] = new Zend_Filter_Input($this->_filters, $this->_validators);
708             }
709         }
710         
711         return self::$_inputFilters[$keyName];
712     }
713     
714     /**
715      * Converts Tinebase_DateTimes into custom representation
716      *
717      * @param array &$_toConvert
718      * @param string $_format
719      * @return void
720      */
721     protected function _convertDateTimeToString(&$_toConvert, $_format)
722     {
723         //$_format = "Y-m-d H:i:s";
724         foreach ($_toConvert as $field => &$value) {
725             if (! $value) {
726                 if (in_array($field, $this->_datetimeFields)) {
727                     $_toConvert[$field] = NULL;
728                 }
729             } elseif ($value instanceof DateTime) {
730                 $_toConvert[$field] = $value->format($_format);
731             } elseif (is_array($value)) {
732                 $this->_convertDateTimeToString($value, $_format);
733             }
734         }
735     }
736     
737     /**
738      * Converts iso8601 formated dates into Tinebase_DateTime representation
739      * 
740      * @param array &$_data
741      * @return void
742      */
743     protected function _convertISO8601ToDateTime(array &$_data)
744     {
745         foreach ($this->_datetimeFields as $field) {
746             if (!isset($_data[$field])) {
747                 continue;
748             }
749             
750             $value = $_data[$field];
751             
752             if ($value instanceof DateTime) {
753                 continue;
754             }
755             
756             if (! is_array($value) && strpos($value, ',') !== false) {
757                 $value = explode(',', $value);
758             }
759             
760             try {
761                 if (is_array($value)) {
762                     foreach ($value as $dataKey => $dataValue) {
763                         if ($dataValue instanceof DateTime) {
764                             continue;
765                         }
766                         
767                         $value[$dataKey] =  (int)$dataValue == 0 ? NULL : new Tinebase_DateTime($dataValue);
768                     }
769                 } else {
770                     $value = (int)$value == 0 ? NULL : new Tinebase_DateTime($value);
771                     
772                 }
773             } catch (Tinebase_DateTime_Exception $zde) {
774                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Error while converting date field "' . $field . '": ' . $zde->getMessage());
775                 $value = NULL;
776             }
777             
778             $_data[$field] = $value;
779         }
780         
781     }
782     
783     /**
784      * cut the timezone-offset from the iso representation in order to force 
785      * Tinebase_DateTime to create dates in the user timezone. otherwise they will be 
786      * created with Etc/GMT+<offset> as timezone which would lead to incorrect 
787      * results in datetime computations!
788      * 
789      * @param  string Tinebase_DateTime::ISO8601 representation of a datetime filed
790      * @return string ISO8601LONG representation ('Y-m-d H:i:s')
791      */
792     protected function _convertISO8601ToISO8601LONG($_ISO)
793     {
794         $cutedISO = preg_replace('/[+\-]{1}\d{2}:\d{2}/', '', $_ISO);
795         $cutedISO = str_replace('T', ' ', $cutedISO);
796         
797         return $cutedISO;
798     }
799     
800     /**
801      * Converts time into iso representation (hh:mm:ss)
802      *
803      * @param array &$_data
804      * @return void
805      * 
806      * @todo    add support for hh:mm:ss AM|PM
807      */
808     protected function _convertTime(&$_data)
809     {
810         foreach ($this->_timeFields as $field) {
811             if (!isset($_data[$field]) || empty($_data[$field])) {
812                 continue;
813             }
814             
815             $hhmmss = explode(":", $_data[$field]);
816             if (count($hhmmss) === 2) {
817                 // seconds missing
818                 $hhmmss[] = '00';
819             }
820             list($hours, $minutes, $seconds) = $hhmmss;
821             if (preg_match('/AM|PM/', $minutes)) {
822                 list($minutes, $notation) = explode(" ", $minutes);
823                 switch($notation) {
824                     case 'AM':
825                         $hours = ($hours == '12') ? 0 : $hours;
826                         break;
827                     case 'PM':
828                         $hours = $hours + 12;
829                         break;
830                 }
831                 $_data[$field] = implode(':', $hhmmss);
832             }
833         }
834     }
835     
836     /**
837      * returns the default filter group for this model
838      * @return string
839      */
840     protected static function _getDefaultFilterGroup()
841     {
842         return get_called_class() . 'Filter';
843     }
844     
845     /**
846      * required by ArrayAccess interface
847      */
848     public function offsetExists($_offset)
849     {
850         return isset($this->_properties[$_offset]);
851     }
852     
853     /**
854      * required by ArrayAccess interface
855      */
856     public function offsetGet($_offset)
857     {
858         return $this->__get($_offset);
859     }
860     
861     /**
862      * required by ArrayAccess interface
863      */
864     public function offsetSet($_offset, $_value)
865     {
866         return $this->__set($_offset, $_value);
867     }
868     
869     /**
870      * required by ArrayAccess interface
871      * @throws Tinebase_Exception_Record_NotAllowed
872      */
873     public function offsetUnset($_offset)
874     {
875         throw new Tinebase_Exception_Record_NotAllowed('Unsetting of properties is not allowed');
876     }
877     
878     /**
879      * required by IteratorAggregate interface
880      */
881     public function getIterator()
882     {
883         return new ArrayIterator($this->_properties);
884     }
885     
886     /**
887      * returns a random 40-character hexadecimal number to be used as 
888      * universal identifier (UID)
889      * 
890      * @param int|optional $_length the length of the uid, defaults to 40
891      * @return string 40-character hexadecimal number
892      */
893     public static function generateUID($_length = false)
894     {
895         $uid = sha1(mt_rand(). microtime());
896         
897         if ($_length !== false) {
898             $uid = substr($uid, 0, $_length);
899         }
900         
901         return $uid;
902     }
903     
904     /**
905     * converts a int, string or Tinebase_Record_Interface to a id
906     *
907     * @param int|string|Tinebase_Record_Abstract $_id the id to convert
908     * @param string $_modelName
909     * @return int|string
910     */
911     public static function convertId($_id, $_modelName = 'Tinebase_Record_Abstract')
912     {
913         if ($_id instanceof $_modelName) {
914             if (! $_id->getId()) {
915                 throw new Tinebase_Exception_InvalidArgument('No id set!');
916             }
917             $id = $_id->getId();
918         } elseif (is_array($_id)) {
919             throw new Tinebase_Exception_InvalidArgument('Id can not be an array!');
920         } else {
921             $id = $_id;
922         }
923     
924         if ($id === 0) {
925             throw new Tinebase_Exception_InvalidArgument($_modelName . '.id can not be 0!');
926         }
927     
928         return $id;
929     }
930     
931     /**
932      * returns a Tinebase_Record_Diff record with differences to the given record
933      * 
934      * @param Tinebase_Record_Interface $_record record for comparison
935      * @param array $omitFields omit fields (for example modlog fields)
936      * @return Tinebase_Record_Diff|NULL
937      */
938     public function diff($_record, $omitFields = array())
939     {
940         if (! $_record instanceof Tinebase_Record_Abstract) {
941             return $_record;
942         }
943         
944         $result = new Tinebase_Record_Diff(array(
945             'id'     => $this->getId(),
946             'model'  => get_class($_record),
947         ));
948         $diff = array();
949         foreach (array_keys($this->_validators) as $fieldName) {
950             if (in_array($fieldName, $omitFields)) {
951                 continue;
952             }
953             
954             $ownField = $this->__get($fieldName);
955             $recordField = $_record->$fieldName;
956             
957             if (in_array($fieldName, $this->_datetimeFields)) {
958                 if ($ownField instanceof DateTime
959                     && $recordField instanceof DateTime) {
960                     
961                     if (! $ownField instanceof Tinebase_DateTime) {
962                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . 
963                             ' Convert ' . $fieldName .' to Tinebase_DateTime to make sure we have the compare() method');
964                         $ownField = new Tinebase_DateTime($ownField);
965                     }
966                         
967                     if ($ownField->compare($recordField) === 0) {
968                         continue;
969                     } else {
970                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
971                             ' datetime for field ' . $fieldName . ' is not equal: '
972                             . $ownField->getIso() . ' != '
973                             . $recordField->getIso()
974                         );
975                     } 
976                 } else if (! $recordField instanceof DateTime && $ownField == $recordField) {
977                     continue;
978                 } 
979             } else if ($fieldName == $this->_identifier && $this->getId() == $_record->getId()) {
980                 continue;
981             } else if ($ownField instanceof Tinebase_Record_Abstract || $ownField instanceof Tinebase_Record_RecordSet) {
982                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
983                     ' Doing subdiff for field ' . $fieldName);
984                 $subdiff = $ownField->diff($recordField);
985                 if (is_object($subdiff) && ! $subdiff->isEmpty()) {
986                     $diff[$fieldName] = $subdiff;
987                 }
988                 continue;
989             } else if ($recordField instanceof Tinebase_Record_Abstract && is_scalar($ownField)) {
990                 // maybe we have the id of the record -> just compare the id
991                 if ($recordField->getId() == $ownField) {
992                     continue;
993                 } else {
994                     $recordField = $recordField->getId();
995                 }
996             } else if ($ownField == $recordField) {
997                 continue;
998             } else if (empty($ownField) && empty($recordField)) {
999                 continue;
1000             }
1001             
1002             $diff[$fieldName] = $recordField;
1003         }
1004         
1005         $result->diff = $diff;
1006         return $result;
1007     }
1008     
1009     /**
1010      * merge given record into $this
1011      * 
1012      * @param Tinebase_Record_Interface $record
1013      * @param Tinebase_Record_Diff $diff
1014      * @return Tinebase_Record_Interface
1015      */
1016     public function merge($record, $diff = null)
1017     {
1018         if (! $this->getId()) {
1019             $this->setId($record->getId());
1020         }
1021         
1022         if ($diff === null) {
1023             $diff = $this->diff($record);
1024         }
1025         
1026         if ($diff === null || empty($diff->diff)) {
1027             return $this;
1028         }
1029         
1030         foreach ($diff->diff as $field => $value) {
1031             if (empty($this->{$field})) {
1032                 $this->{$field} = $value;
1033             }
1034         }
1035         
1036         return $this;
1037     }
1038     
1039     /**
1040      * check if data got modified
1041      * 
1042      * @return boolean
1043      */
1044     public function isDirty()
1045     {
1046         return $this->_isDirty;
1047     }
1048     
1049     /**
1050      * returns TRUE if given record obsoletes this one
1051      * 
1052      * @param  Tinebase_Record_Interface $_record
1053      * @return bool
1054      */
1055     public function isObsoletedBy($_record)
1056     {
1057         if (get_class($_record) !== get_class($this)) {
1058             throw new Tinebase_Exception_InvalidArgument('Records could not be compared');
1059         } else if ($this->getId() && $_record->getId() !== $this->getId()) {
1060             throw new Tinebase_Exception_InvalidArgument('Record id mismatch');
1061         }
1062         
1063         if ($this->has('seq') && $_record->seq != $this->seq) {
1064             return $_record->seq > $this->seq;
1065         }
1066         
1067         return ($this->has('last_modified_time')) ? $_record->last_modified_time > $this->last_modified_time : TRUE;
1068     }
1069     
1070     /**
1071      * check if two records are equal
1072      * 
1073      * @param  Tinebase_Record_Interface $_record record for comparism
1074      * @param  array                     $_toOmit fields to omit
1075      * @return bool
1076      */
1077     public function isEqual($_record, array $_toOmit = array())
1078     {
1079         $diff = $this->diff($_record);
1080         return ($diff) ? $diff->isEmpty($_toOmit) : FALSE;
1081     }
1082     
1083     /**
1084      * translate this records' fields
1085      *
1086      */
1087     public function translate()
1088     {
1089         // get translation object
1090         if (!empty($this->_toTranslate)) {
1091             $translate = Tinebase_Translation::getTranslation($this->_application);
1092             
1093             foreach ($this->_toTranslate as $field) {
1094                 $this->$field = $translate->_($this->$field);
1095             }
1096         }
1097     }
1098
1099     /**
1100      * check if the model has a specific field (container_id for example)
1101      *
1102      * @param string $_field
1103      * @return boolean
1104      */
1105     public function has($_field) 
1106     {
1107         return ((isset($this->_validators[$_field]) || array_key_exists ($_field, $this->_validators)));
1108     }   
1109
1110     /**
1111      * get fields
1112      * 
1113      * @return array
1114      */
1115     public function getFields()
1116     {
1117         return array_keys($this->_validators);
1118     }
1119     
1120     /**
1121      * fills a record from json data
1122      *
1123      * @param string $_data json encoded data
1124      * @return void
1125      * 
1126      * @todo replace this (and setFromJsonInUsersTimezone) with Tinebase_Convert_Json::toTine20Model
1127      * @todo move custom _setFromJson to (custom) converter
1128      */
1129     public function setFromJson($_data)
1130     {
1131         if (is_array($_data)) {
1132             $recordData = $_data;
1133         } else {
1134             $recordData = Zend_Json::decode($_data);
1135         }
1136         
1137         // sanitize container id if it is an array
1138         if ($this->has('container_id') && isset($recordData['container_id']) && is_array($recordData['container_id']) && isset($recordData['container_id']['id']) ) {
1139             $recordData['container_id'] = $recordData['container_id']['id'];
1140         }
1141         
1142         $this->_setFromJson($recordData);
1143         $this->setFromArray($recordData);
1144     }
1145     
1146     /**
1147      * can be reimplemented by subclasses to modify values during setFromJson
1148      * @param array $_data the json decoded values
1149      * @return void
1150      */
1151     protected function _setFromJson(array &$_data)
1152     {
1153         
1154     }
1155
1156     /**
1157      * returns modlog omit fields
1158      *
1159      * @return array
1160      */
1161     public function getModlogOmitFields()
1162     {
1163         return $this->_modlogOmitFields;
1164     }
1165
1166     /**
1167      * returns read only fields
1168      *
1169      * @return array
1170      */
1171     public function getReadOnlyFields()
1172     {
1173         return $this->_readOnlyFields;
1174     }
1175
1176     /**
1177      * sets the non static properties by the created configuration object on instantiation
1178      */
1179     protected function _setFromConfigurationObject()
1180     {
1181         // set protected, non static properties
1182         $co = static::getConfiguration();
1183         if ($co && $mc = $co->toArray()) {
1184             foreach ($mc as $property => $value) {
1185                 $this->{$property} = $value;
1186             }
1187         }
1188     }
1189
1190     /**
1191      * returns the title of the record
1192      * 
1193      * @return string
1194      */
1195     public function getTitle()
1196     {
1197         $c = static::getConfiguration();
1198         
1199         // TODO: fallback, remove if all models use modelconfiguration
1200         if (! $c) {
1201             return $this->title;
1202         }
1203         
1204         // use vsprintf formatting if it is an array
1205         if (is_array($c->titleProperty)) {
1206             if (! is_array($c->titleProperty[1])) {
1207                 $propertyValues = array($this->{$c->titleProperty[1]});
1208             } else {
1209                 $propertyValues = array();
1210                 foreach($c->titleProperty[1] as $property) {
1211                     $propertyValues[] = $this->{$property};
1212                 }
1213             }
1214             return vsprintf($c->titleProperty[0], $propertyValues);
1215         } else {
1216             return $this->{$c->titleProperty};
1217         }
1218     }
1219     
1220     /**
1221      * returns the foreignId fields (used in Tinebase_Convert_Json)
1222      * @return array
1223      */
1224     public static function getResolveForeignIdFields()
1225     {
1226         return static::$_resolveForeignIdFields;
1227     }
1228     
1229     /**
1230      * returns all textfields having labels for the autocomplete field function
1231      * 
1232      * @return array
1233      */
1234     public static function getAutocompleteFields()
1235     {
1236         $keys = array();
1237         
1238         foreach (self::getConfiguration()->getFields() as $key => $fieldDef) {
1239             if ($fieldDef['type'] == 'string' || $fieldDef['type'] == 'text') {
1240                 $keys[] = $key;
1241             }
1242         }
1243         
1244         return $keys;
1245     }
1246 }