0010560: Import contacts using merge mine
[tine20] / tine20 / Tinebase / Import / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Import
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Cornelius Weiss <c.weiss@metaways.de>
9  * @copyright   Copyright (c) 2010-2013 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * abstract Tinebase Import
14  * 
15  * @package Tinebase
16  * @subpackage  Import
17  */
18 abstract class Tinebase_Import_Abstract implements Tinebase_Import_Interface
19 {
20     /**
21      * import result array
22      * 
23      * @var array
24      */
25     protected $_importResult = array(
26         'results'           => NULL,
27         'exceptions'        => NULL,
28         'totalcount'        => 0,
29         'updatecount'       => 0,
30         'failcount'         => 0,
31         'duplicatecount'    => 0,
32     );
33     
34     /**
35      * possible configs with default values
36      * 
37      * @var array
38      */
39     protected $_options = array(
40         'dryrun'            => false,
41         'updateMethod'      => 'update',
42         'createMethod'      => 'create',
43         'model'             => '',
44         'shared_tags'       => 'create', //'onlyexisting',
45         'autotags'          => array(),
46         'encoding'          => 'auto',
47         'encodingTo'        => 'UTF-8',
48         'useStreamFilter'   => true,
49         'postMappingHook'   => null,
50         // if this is set, always resolve duplicates
51         'duplicateResolveStrategy' => null,
52     );
53     
54     /**
55      * additional config options (to be added by child classes)
56      * 
57      * @var array
58      */
59     protected $_additionalOptions = array();
60     
61     /**
62      * the record controller
63      *
64      * @var Tinebase_Controller_Record_Interface
65      */
66     protected $_controller = NULL;
67     
68     /**
69      * constructs a new importer from given config
70      * 
71      * @param array $_options
72      */
73     public function __construct(array $_options = array())
74     {
75         $this->_options = array_merge($this->_options, $this->_additionalOptions);
76         
77         foreach($_options as $key => $cfg) {
78             if ((isset($this->_options[$key]) || array_key_exists($key, $this->_options))) {
79                 $this->_options[$key] = $cfg;
80             }
81         }
82         
83         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
84             . ' Creating importer with following config: ' . print_r($this->_options, TRUE));
85     }
86     
87     /**
88      * import given filename
89      * 
90      * @param string $_filename
91      * @param array $_clientRecordData
92      * @return @see{Tinebase_Import_Interface::import}
93      */
94     public function importFile($_filename, $_clientRecordData = array())
95     {
96         if (preg_match('/^win/i', PHP_OS)) {
97            $_filename = utf8_decode($_filename);
98         }
99         if (! file_exists($_filename)) {
100             throw new Tinebase_Exception_NotFound("File $_filename not found.");
101         }
102         $resource = fopen($_filename, 'r');
103         
104         $retVal = $this->import($resource, $_clientRecordData);
105         fclose($resource);
106         
107         return $retVal;
108     }
109     
110     /**
111      * import from given data
112      * 
113      * @param string $_data
114      * @param array $_clientRecordData
115      * @return @see{Tinebase_Import_Interface::import}
116      */
117     public function importData($_data, $_clientRecordData = array())
118     {
119         $resource = fopen('php://memory', 'w+');
120         fwrite($resource, $_data);
121         rewind($resource);
122         
123         $retVal = $this->import($resource);
124         fclose($resource);
125         
126         return $retVal;
127     }
128     
129     /**
130      * import the data
131      *
132      * @param resource $_resource (if $_filename is a stream)
133      * @param array $_clientRecordData
134      * @return array with import data (imported records, failures, duplicates and totalcount)
135      */
136     public function import($_resource = NULL, $_clientRecordData = array())
137     {
138         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
139             . ' Starting import of ' . ((! empty($this->_options['model'])) ? $this->_options['model'] . 's' : ' records'));
140         
141         $this->_initImportResult();
142         $this->_appendStreamFilters($_resource);
143         $this->_beforeImport($_resource);
144         $this->_doImport($_resource, $_clientRecordData);
145         $this->_logImportResult();
146         
147         return $this->_importResult;
148     }
149     
150     /**
151      * append stream filter for correct linebreaks
152      * - iconv with IGNORE
153      * - replace linebreaks
154      * 
155      * @param resource $resource
156      */
157     protected function _appendStreamFilters($resource)
158     {
159         if (! $resource || ! isset($this->_options['useStreamFilter']) || ! $this->_options['useStreamFilter']) {
160             return;
161         }
162
163         if (! isset($this->_options['encoding']) || $this->_options['encoding'] === 'auto' && extension_loaded('mbstring')) {
164             require_once 'StreamFilter/ConvertMbstring.php';
165             $filter = 'convert.mbstring';
166         } else if (isset($this->_options['encoding']) && $this->_options['encoding'] !== $this->_options['encodingTo']) {
167             $filter = 'convert.iconv.' . $this->_options['encoding'] . '/' . $this->_options['encodingTo'] . '//IGNORE';
168         } else {
169             $filter = NULL;
170         }
171             
172         if ($filter !== NULL) {
173             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
174                 . ' Add convert stream filter: ' . $filter);
175             stream_filter_append($resource, $filter);
176         }
177         
178         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
179             . ' Adding streamfilter for correct linebreaks');
180         require_once 'StreamFilter/StringReplace.php';
181         $filter = stream_filter_append($resource, 'str.replace', STREAM_FILTER_READ, array(
182             'search'            => '/\r\n{0,1}/',
183             'replace'           => "\r\n",
184             'searchIsRegExp'    => TRUE
185         ));
186     }
187     
188     /**
189      * init import result data
190      */
191     protected function _initImportResult()
192     {
193         $this->_importResult['results']     = (! empty($this->_options['model'])) ? new Tinebase_Record_RecordSet($this->_options['model']) : array();
194         $this->_importResult['exceptions']  = new Tinebase_Record_RecordSet('Tinebase_Model_ImportException');
195     }
196     
197     /**
198      * do something before the import
199      * 
200      * @param resource $_resource
201      */
202     protected function _beforeImport($_resource = NULL)
203     {
204     }
205     
206     /**
207      * do import: loop data -> convert to records -> import records
208      * 
209      * @param mixed $_resource
210      * @param array $_clientRecordData
211      */
212     protected function _doImport($_resource = NULL, $_clientRecordDatas = array())
213     {
214         $clientRecordDatas = $this->_sortClientRecordsByIndex($_clientRecordDatas);
215         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
216             . ' Client record data: ' . print_r($clientRecordDatas, TRUE));
217         
218         $recordIndex = 0;
219         while (($recordData = $this->_getRawData($_resource)) !== FALSE) {
220             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
221                 . ' Importing record ' . $recordIndex . ' ...');
222             $recordToImport = NULL;
223             try {
224                 // client record overwrites record in import data (only if set)
225                 $clientRecordData = (isset($clientRecordDatas[$recordIndex]) || array_key_exists($recordIndex, $clientRecordDatas)) ? $clientRecordDatas[$recordIndex]['recordData'] : NULL;
226                 if ($clientRecordData && Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
227                     Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Client record: ' . print_r($clientRecordData, TRUE));
228                 }
229                 
230                 // NOTE _processRawData might return multiple recordDatas
231                 // NOTE $clientRecordData is always one record
232                 $recordDataToImport = $clientRecordData ? array($clientRecordData) : $this->_processRawData($recordData);
233                 $resolveStrategy = $clientRecordData ? $clientRecordDatas[$recordIndex]['resolveStrategy'] : NULL;
234                 
235                 foreach ($recordDataToImport as $idx => $processedRecordData) {
236                     $recordToImport = $this->_createRecordToImport($processedRecordData);
237                     if ($resolveStrategy !== 'discard') {
238                         $importedRecord = $this->_importRecord($recordToImport, $resolveStrategy, $processedRecordData);
239                     } else {
240                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
241                                 . ' Discarding record ' . $recordIndex);
242                         
243                         // just add autotags to record (if id is available)
244                         if ($recordToImport->getId()) {
245                             $record = call_user_func(array($this->_controller, 'get'), $recordToImport->getId());
246                             $this->_addAutoTags($record);
247                             call_user_func(array($this->_controller, $this->_options['updateMethod']), $record);
248                         }
249                     }
250                 }
251             } catch (Exception $e) {
252                 $this->_handleImportException($e, $recordIndex, $recordToImport);
253             }
254             $recordIndex++;
255         }
256     }
257     
258     /**
259      * sort client data array
260      * 
261      * @param array $_clientRecordData
262      * @return array
263      */
264     protected function _sortClientRecordsByIndex($_clientRecordData)
265     {
266         $result = array();
267         
268         foreach ($_clientRecordData as $data) {
269             if (isset($data['index'])) {
270                 $result[$data['index']] = $data;
271             }
272         }
273         
274         return $result;
275     }
276     
277     /**
278      * process raw data (mapping + conversions)
279      * 
280      * NOTE: returns empty Traversable if record should be skipped on purpose
281      * NOTE: If there will occur any import error the client management will only work for 1 imported entry
282      *       and not if multiple were stored in ArrayObject
283      *
284      * @param array $_data
285      * @return Traversable
286      * @throws Tinebase_Exception_Record_Validation on broken mapping
287      */
288     protected function _processRawData($_data)
289     {
290         $result = array();
291         
292         $mappedData = $this->_doMapping($_data);
293         
294         if ((isset($this->_options["postMappingHook"]) || array_key_exists("postMappingHook", $this->_options))) {
295             if (isset($this->_options['postMappingHook']['path'])) {
296                $mappedData = $this->_postMappingHook($mappedData);
297             }
298         }
299         
300         if (empty($mappedData) && empty($_data)) {
301             Throw new Tinebase_Exception_UnexpectedValue("_processRawData got no data and could not map any.");
302         }
303         
304         $mappedData = $mappedData instanceof ArrayObject ? $mappedData : new ArrayObject(array($mappedData), ArrayObject::STD_PROP_LIST);
305         
306         if (! empty($mappedData)) {
307             foreach ($mappedData as $idx => $recordArray) {
308                 $convertedData = $this->_doConversions($recordArray);
309                 $mappedData[$idx] = array_merge($convertedData, $this->_addData($convertedData));
310             }
311             $result = $mappedData;
312             
313             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
314                 . ' Merged data: ' . print_r($result, true));
315         } else {
316             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
317                 . ' Got empty record from mapping! Was: ' . print_r($_data, TRUE));
318         }
319         return $result;
320     }
321     
322     /**
323      * do the mapping and replacements
324      *
325      * @param array $_data
326      * @return array
327      */
328     protected function _doMapping($_data)
329     {
330         return $_data;
331     }
332     
333     /**
334      * Runs a user defined script
335      *
336      * After your data are mapped and the hook is enabled in your definition every data set will be
337      *  parsed through this hook.
338      * 
339      * It will convert the data set to a json object and send it to the stdin of a script.
340      *  The script should usualy print a json object of the extended, manipulated or corrected data set. 
341      * 
342      * But if you intend to split the data to two or more sets or import data from different sources
343      *  you have to print a json array.
344      * 
345      * @param array $data
346      * @return Array|ArrayObject
347      */
348     protected function _postMappingHook ($data)
349     {
350         $jsonEncodedData = Zend_Json::encode($data);
351         $jsonDecodedData = null;
352         
353         $script = $this->_options['postMappingHook']['path'];
354         //The path given in the xml is not dynamic. Therefore it must be absolute or relative to the tine20 directory.
355         if ($script[0] !== DIRECTORY_SEPARATOR) {
356             $script = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . $script;
357         }
358         if (! is_executable($script)) {
359             throw new Tinebase_Exception_UnexpectedValue("Script does not exists or isn't executable. Path: " . $script);
360         }
361         
362         $jDataToSend =  escapeshellarg($jsonEncodedData);
363         $jsonReceivedData = shell_exec(escapeshellcmd($script) . " $jDataToSend");
364         
365         $returnJDecodedData = Zend_Json_Decoder::decode($jsonReceivedData);
366         if (! $returnJDecodedData) {
367             throw new Tinebase_Exception_UnexpectedValue("Something went wrong by decoding the received json data!");
368         }
369         
370         if (strpos($jsonReceivedData, '[') === 0) {
371             $return = new ArrayObject(array(), ArrayObject::STD_PROP_LIST);
372             foreach ($returnJDecodedData as $key => $val)
373                 $return[$key] = $val;
374         } else {
375             $return = $returnJDecodedData;
376         }
377         
378         return $return;
379     }
380     
381     /**
382      * do conversions (transformations, charset, replacements ...)
383      *
384      * @param array $_data
385      * @return array
386      * 
387      * @todo add date and other conversions
388      * @todo add generic mechanism for value pre/postfixes? (see accountLoginNamePrefix in Admin_User_Import)
389      */
390     protected function _doConversions($_data)
391     {
392         if (isset($this->_options['mapping'])) {
393             $data = $this->_doMappingConversion($_data);
394         } else {
395             $data = $_data;
396         }
397         
398         foreach ($data as $key => $value) { 
399             $data[$key] = $this->_convertEncoding($value);
400         }
401         
402         return $data;
403     }
404     
405     /**
406      * convert encoding
407      * NOTE: always do encoding with //IGNORE as we do not know the actual encoding in some cases
408      * 
409      * @param string|array $_value
410      * @return string|array
411      */
412     protected function _convertEncoding($_value)
413     {
414         if (empty($_value) || (! isset($this->_options['encodingTo']) || (isset($this->_options['useStreamFilter']) && $this->_options['useStreamFilter']))) {
415             return $_value;
416         }
417         
418         if (is_array($_value)) {
419             $result = array();
420             foreach ($_value as $singleValue) {
421                 $result[] = $this->_doConvert($singleValue);
422             }
423         } else {
424             $result = $this->_doConvert($_value);
425         }
426         
427         return $result;
428     }
429     
430     /**
431      * convert string with iconv or mb_convert_encoding
432      * 
433      * @param string $string
434      * @return string
435      */
436     protected function _doConvert($string)
437     {
438         if ((! isset($this->_options['encoding']) || $this->_options['encoding'] === 'auto') && extension_loaded('mbstring')) {
439             $encoding = mb_detect_encoding($string, array('utf-8', 'iso-8859-1', 'windows-1252', 'iso-8859-15'));
440             if ($encoding !== FALSE) {
441                 $encodingFn = 'mb_convert_encoding';
442                 $result = @mb_convert_encoding($string, $this->_options['encodingTo'], $encoding);
443             }
444         } else if (isset($this->_options['encoding'])) {
445             $encoding = $this->_options['encoding'];
446             $encodingFn = 'iconv';
447             $result = @iconv($encoding, $this->_options['encodingTo'] . '//TRANSLIT', $string);
448         } else {
449             return $string;
450         }
451
452         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
453             . ' Encoded ' . $string . ' from ' . $encoding . ' to ' . $this->_options['encodingTo'] . ' using ' . $encodingFn . ' . => ' . $result);
454         return $result;
455     }
456     
457     /**
458      * do the mapping conversions defined in field configs
459      *
460      * @param array $_data
461      * @return array
462      */
463     protected function _doMappingConversion($_data)
464     {
465         $data = $_data;
466         foreach ($this->_options['mapping']['field'] as $index => $field) {
467             if (! (isset($field['destination']) || array_key_exists('destination', $field)) || $field['destination'] == '' || ! isset($_data[$field['destination']])) {
468                 continue;
469             }
470         
471             $key = $field['destination'];
472         
473             if (isset($field['replace'])) {
474                 if ($field['replace'] === '\n') {
475                     $data[$key] = str_replace("\\n", "\r\n", $_data[$key]);
476                 }
477             } else if (isset($field['separator'])) {
478                 $data[$key] = preg_split('/\s*' . $field['separator'] . '\s*/', $_data[$key]);
479             } else if (isset($field['fixed'])) {
480                 $data[$key] = $field['fixed'];
481             } else if (isset($field['append'])) {
482                 $data[$key] .= $field['append'] . $_data[$key];
483             } else if (isset($field['typecast'])) {
484                 switch ($field['typecast']) {
485                     case 'int':
486                     case 'integer':
487                         $data[$key] = (integer) $_data[$key];
488                         break; 
489                     case 'string':
490                         $data[$key] = (string) $_data[$key];
491                         break;
492                     case 'bool':
493                     case 'boolean':
494                         $data[$key] = (string) $_data[$key];
495                         break;
496                     case 'datetime':
497                         if (isset($_data[$key])) {
498                             $datetime = isset($field["datetime_pattern"]) ?
499                                 DateTime::createFromFormat($field["datetime_pattern"], $_data[$key]) :
500                                 new DateTime($_data[$key]);
501                             
502                             $data[$key] = $datetime instanceof DateTime ? $datetime->format('Y-m-d H:i:s') : null;
503                         }
504                         break;
505                     default:
506                         $data[$key] = $_data[$key];
507                 }
508             } else {
509                 $data[$key] = $_data[$key];
510             }
511         }
512         
513         return $data;
514     }
515
516     /**
517      * add some more values (overwrite that if you need some special/dynamic fields)
518      *
519      * @param  array recordData
520      */
521     protected function _addData()
522     {
523         return array();
524     }
525     
526     /**
527      * create record from record data
528      * 
529      * @param array $_recordData
530      * @return Tinebase_Record_Abstract
531      */
532     protected function _createRecordToImport($_recordData)
533     {
534         $record = new $this->_options['model'](array(), TRUE);
535         $record->setFromJsonInUsersTimezone($_recordData);
536         
537         return $record;
538     }
539     
540     /**
541      * import single record
542      *
543      * @param Tinebase_Record_Abstract $_record
544      * @param string $_resolveStrategy
545      * @param array $_recordData not needed here but in other import classes (i.a. Admin_Import_Csv)
546      * @return void
547      * @throws Tinebase_Exception_Record_Validation
548      */
549     protected function _importRecord($_record, $_resolveStrategy = NULL, $_recordData = array())
550     {
551         $_record->isValid(TRUE);
552         
553         if ($this->_options['dryrun']) {
554             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
555         }
556         
557         $this->_handleTags($_record, $_resolveStrategy);
558         
559         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
560             . ' Record to import: ' . print_r($_record->toArray(), true));
561         
562         $importedRecord = $this->_importAndResolveConflict($_record, $_resolveStrategy);
563         
564         $this->_importResult['results']->addRecord($importedRecord);
565         
566         if ($this->_options['dryrun']) {
567             Tinebase_TransactionManager::getInstance()->rollBack();
568         } else if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
569             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Successfully imported record with id ' . $importedRecord->getId());
570         }
571         
572         $this->_importResult['totalcount']++;
573     }
574     
575     /**
576      * handle record tags
577      * 
578      * @param Tinebase_Record_Abstract $_record
579      * @param string $_resolveStrategy
580      */
581     protected function _handleTags($_record, $_resolveStrategy = NULL)
582     {
583         if (isset($_record->tags) && is_array($_record->tags)) {
584             $_record->tags = $this->_addSharedTags($_record->tags);
585         } else {
586             $_record->tags = NULL;
587         }
588         
589         if ($_resolveStrategy === NULL && ! empty($this->_options['autotags'])) {
590             // only add autotags for "new" records
591             $this->_addAutoTags($_record);
592         }
593     }
594     
595     /**
596     * add/create shared tags if they don't exist
597     *
598     * @param   array $_tags array of tag strings
599     * @return  Tinebase_Record_RecordSet with Tinebase_Model_Tag
600     */
601     protected function _addSharedTags($_tags)
602     {
603         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Adding tags: ' . print_r($_tags, TRUE));
604     
605         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tag');
606         foreach ($_tags as $tagData) {
607             $tagData = (is_array($tagData)) ? $tagData : array('name' => $tagData);
608             $tagName = trim($tagData['name']);
609     
610             // only check non-empty tags
611             if (empty($tagName)) {
612                 continue;
613             }
614     
615             $createTag = (isset($this->_options['shared_tags']) && $this->_options['shared_tags'] == 'create');
616             $tagToAdd = $this->_getSingleTag($tagName, $tagData, $createTag);
617             if ($tagToAdd) {
618                 $result->addRecord($tagToAdd);
619             }
620         }
621         
622         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
623             . ' ' . print_r($result->toArray(), TRUE));
624     
625         return $result;
626     }
627     
628     /**
629      * get tag / create on the fly
630      * 
631      * @param string $_name
632      * @param array $_tagData
633      * @param boolean $_create
634      * @return Tinebase_Model_Tag
635      */
636     protected function _getSingleTag($_name, $_tagData = array(), $_create = TRUE)
637     {
638         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
639             . ' Tag name: ' . $_name . ' / data: ' . print_r($_tagData, TRUE));
640         
641         $name = $_name;
642         if (isset($_tagData['name'])) {
643             $_tagData['name'] = $name;
644         }
645         
646         $tag = NULL;
647         
648         if (isset($_tagData['id'])) {
649             try {
650                 $tag = Tinebase_Tags::getInstance()->get($_tagData['id']);
651                 return $tag;
652             } catch (Tinebase_Exception_NotFound $tenf) {
653                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
654                     . ' Could not find tag by id: ' . $_tagData['id']);
655             }
656         }
657         
658         try {
659             $tag = Tinebase_Tags::getInstance()->getTagByName($name, Tinebase_Model_TagRight::USE_RIGHT, NULL);
660             return $tag;
661         } catch (Tinebase_Exception_NotFound $tenf) {
662             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
663                 . ' Could not find tag by name: ' . $name);
664         }
665         
666         if ($_create) {
667             $tagData = (! empty($_tagData)) ? $_tagData : array(
668                 'name' => $name,
669             );
670             $tag = $this->_createTag($tagData);
671         }
672         
673         return $tag;
674     }
675     
676     /**
677      * create new tag
678      * 
679      * @param array $_tagData
680      * @return Tinebase_Model_Tag
681      * 
682      * @todo allow to set contexts / application / rights
683      * @todo only ignore acl for autotags that are present in import definition
684      */
685     protected function _createTag($_tagData)
686     {
687         $description  = substr((isset($_tagData['description'])) ? $_tagData['description'] : $_tagData['name'] . ' (imported)', 0, 50);
688         $type         = (isset($_tagData['type']) && ! empty($_tagData['type'])) ? $_tagData['type'] : Tinebase_Model_Tag::TYPE_SHARED;
689         $color        = (isset($_tagData['color'])) ? $_tagData['color'] : '#ffffff';
690         
691         $newTag = new Tinebase_Model_Tag(array(
692             'name'          => $_tagData['name'],
693             'description'   => $description,
694             'type'          => strtolower($type),
695             'color'         => $color,
696             'type'          => $type,
697         ));
698         
699         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
700             . ' Creating new ' . $type . ' tag: ' . $_tagData['name']);
701         
702         $tag = Tinebase_Tags::getInstance()->createTag($newTag, TRUE);
703         
704         // @todo should be moved to Tinebase_Tags / always be done for all kinds of tags on create
705         if ($type === Tinebase_Model_Tag::TYPE_SHARED) {
706             $right = new Tinebase_Model_TagRight(array(
707                 'tag_id'        => $newTag->getId(),
708                 'account_type'  => Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE,
709                 'account_id'    => 0,
710                 'view_right'    => TRUE,
711                 'use_right'     => TRUE,
712             ));
713             Tinebase_Tags::getInstance()->setRights($right);
714             Tinebase_Tags::getInstance()->setContexts(array('any'), $newTag->getId());
715         }
716         
717         return $tag;
718     }
719     
720     /**
721     * add auto tags from options
722     *
723     * @param Tinebase_Record_Abstract $_record
724     */
725     protected function _addAutoTags($_record)
726     {
727         $autotags = $this->_sanitizeAutotagsOption();
728         
729         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
730             ' Trying to add ' . count($autotags) . ' autotag(s) to record.');
731         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($autotags, TRUE));
732         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_record->toArray(), TRUE));
733         
734         $tags = ($_record->tags instanceof Tinebase_Record_RecordSet) ? $_record->tags : new Tinebase_Record_RecordSet('Tinebase_Model_Tag');
735         foreach ($autotags as $tagData) {
736             if (is_string($tagData)) {
737                 try {
738                     $tag = Tinebase_Tags::getInstance()->get($tagData);
739                 } catch (Tinebase_Exception_NotFound $tenf) {
740                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $tenf);
741                     $tag = NULL;
742                 }
743             } else {
744                 $tagData = $this->_doAutoTagReplacements($tagData);
745                 $tag = $this->_getSingleTag($tagData['name'], $tagData);
746             }
747             if ($tag !== NULL) {
748                 $tags->addRecord($tag);
749             }
750         }
751         $_record->tags = $tags;
752         
753         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($tags->toArray(), TRUE));
754     }
755     
756     /**
757      * replace some strings in autotags (name + description)
758      * 
759      * @param array $_tagData
760      * @return array
761      */
762     protected function _doAutoTagReplacements($_tagData)
763     {
764         $result = $_tagData;
765         
766         $search = array(
767             '###CURRENTDATE###', 
768             '###CURRENTTIME###', 
769             '###USERFULLNAME###'
770         );
771         $now = Tinebase_DateTime::now();
772         $replacements = array(
773             Tinebase_Translation::dateToStringInTzAndLocaleFormat($now, NULL, NULL, 'date'),
774             Tinebase_Translation::dateToStringInTzAndLocaleFormat($now, NULL, NULL, 'time'),
775             Tinebase_Core::getUser()->accountDisplayName
776         );
777         $fields = array('name', 'description');
778         
779         foreach ($fields as $field) {
780             if (isset($result[$field])) {
781                 $result[$field] = str_replace($search, $replacements, $result[$field]);
782             }
783         }
784         
785         return $result;
786     }
787     
788     /**
789      * sanitize autotag option
790      * 
791      * @return array
792      */
793     protected function _sanitizeAutotagsOption()
794     {
795         $autotags = ((isset($this->_options['autotags']['tag']) || array_key_exists('tag', $this->_options['autotags'])) && count($this->_options['autotags']) == 1) 
796             ? $this->_options['autotags']['tag'] : $this->_options['autotags'];
797
798         $autotags = ((isset($autotags['name']) || array_key_exists('name', $autotags))) ? array($autotags) : $autotags;
799         
800         if ((isset($autotags['tag']) || array_key_exists('tag', $autotags))) {
801             unset($autotags['tag']);
802         }
803         
804         return $autotags;
805     }
806     
807     /**
808      * import record and resolve possible conflicts
809      * 
810      * supports $_resolveStrategy(s): ['mergeTheirs', ('Merge, keeping existing details')],
811      *                                ['mergeMine',   ('Merge, keeping my details')],
812      *                                ['keep',        ('Keep both records')]
813      * 
814      * @param Tinebase_Record_Abstract $record
815      * @param string $resolveStrategy
816      * @param Tinebase_Record_Abstract $clientRecord
817      * @return Tinebase_Record_Abstract
818      * 
819      * @todo we should refactor the merge handling: this function should always get the merged record OR always do the merging itself
820      */
821     protected function _importAndResolveConflict(Tinebase_Record_Abstract $record, $resolveStrategy = null, $clientRecord = null)
822     {
823         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
824             . ' ResolveStrategy: ' . $resolveStrategy);
825         if ($clientRecord && Tinebase_Core::isLogLevel(Zend_Log::TRACE) && $clientRecord) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
826             . ' Client record: ' . print_r($clientRecord->toArray(), TRUE));
827         
828         switch ($resolveStrategy) {
829             case 'mergeTheirs':
830             case 'mergeMine':
831                 if ($clientRecord) {
832                     if ($resolveStrategy === 'mergeTheirs') {
833                         $recordToUpdate = $this->_mergeRecord($record, $clientRecord);
834                     } else {
835                         $recordToUpdate = $this->_mergeRecord($clientRecord, $record);
836                     }
837                 } else {
838                     $recordToUpdate = $record;
839                 }
840                 
841                 if ($recordToUpdate !== null) {
842                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE) && $clientRecord) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
843                         . ' Merged record: ' . print_r($record->toArray(), TRUE));
844                     
845                     $record = call_user_func(array($this->_controller, $this->_options['updateMethod']), $recordToUpdate, FALSE);
846                 }
847                 
848                 break;
849             case 'keep':
850                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
851                     . ' Record to import (keep both / no duplicate check): ' . print_r($record->toArray(), TRUE));
852                 
853                 $record = call_user_func(array($this->_controller, $this->_options['createMethod']), $record, FALSE);
854                 break;
855             default:
856                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
857                     . ' Record to import: ' . print_r($record->toArray(), TRUE));
858                 
859                 $record = call_user_func(array($this->_controller, $this->_options['createMethod']), $record);
860         }
861         
862         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
863             . ' record: ' . print_r($record->toArray(), TRUE));
864         
865         return $record;
866     }
867     
868     /**
869      * merge record / skip if no diff
870      * 
871      * @param Tinebase_Record_Abstract $updateRecord
872      * @param Tinebase_Record_Abstract $mergeRecord
873      * @return Tinebase_Record_Abstract
874      */
875     protected function _mergeRecord($updateRecord, $mergeRecord)
876     {
877         $omitFields = array(
878             'creation_time',
879             'created_by',
880             'last_modified_time',
881             'last_modified_by',
882             'seq',
883             'id'
884         );
885         
886         $diff = $updateRecord->diff($mergeRecord, $omitFields);
887         if (! $diff || $diff->isEmpty()) {
888             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
889                 . ' Records are identical, no need to update');
890             return null;
891         } else {
892             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
893                 . ' Got diff: ' . print_r($diff->diff, TRUE));
894         }
895         
896         return $updateRecord->merge($mergeRecord, $diff);
897     }
898     
899     /**
900      * handle import exceptions
901      * 
902      * @param Exception $e
903      * @param integer $recordIndex
904      * @param Tinebase_Record_Abstract|array $record
905      * @param boolean $allowToResolveDuplicates
906      * 
907      * @todo use json converter for client record
908      */
909     protected function _handleImportException(Exception $e, $recordIndex, $record = null, $allowToResolveDuplicates = true)
910     {
911         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage());
912         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
913         
914         if ($e instanceof Tinebase_Exception_Duplicate) {
915             $exception = $this->_handleDuplicateExceptions($e, $recordIndex, $record, $allowToResolveDuplicates);
916         } else {
917             $this->_importResult['failcount']++;
918             $exception = array(
919                 'code'         => $e->getCode(),
920                 'message'      => $e->getMessage(),
921                 'clientRecord' => ($record !== NULL && $record instanceof Tinebase_Record_Abstract) ? $record->toArray() 
922                     : (is_array($record) ? $record : array()),
923             );
924         }
925         
926         if ($exception) {
927             $this->_importResult['exceptions']->addRecord(new Tinebase_Model_ImportException(array(
928                 'code'          => $e->getCode(),
929                 'message'       => $e->getMessage(),
930                 'exception'     => $exception,
931                 'index'         => $recordIndex,
932             )));
933         }
934     }
935     
936     /**
937      * handle duplicate exceptions
938      * 
939      * @param Tinebase_Exception_Duplicate $ted
940      * @param integer $recordIndex
941      * @param Tinebase_Record_Abstract|array $record
942      * @param boolean $allowToResolveDuplicates
943      * @return array|null exception
944      */
945     protected function _handleDuplicateExceptions(Tinebase_Exception_Duplicate $ted, $recordIndex, $record = null, $allowToResolveDuplicates = true)
946     {
947         if (! empty($this->_options['duplicateResolveStrategy']) && $allowToResolveDuplicates) {
948             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
949                 . ' Trying to resolve with configured strategy: ' . $this->_options['duplicateResolveStrategy']);
950             
951             try {
952                 $updatedRecord = $this->_importAndResolveConflict($ted->getData()->getFirstRecord(), $this->_options['duplicateResolveStrategy'], $ted->getClientRecord());
953                 $this->_importResult['updatecount']++;
954                 $this->_importResult['results']->addRecord($updatedRecord);
955             } catch (Exception $newException) {
956                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
957                     . " Resolving failed. Don't try to resolve duplicates this time");
958                 
959                 $this->_handleImportException($newException, $recordIndex, $record, false);
960             }
961             $result = null;
962         } else {
963             $this->_importResult['duplicatecount']++;
964             $result = $ted->toArray();
965         }
966         
967         return $result;
968     }
969     
970     /**
971      * log import result
972      */
973     protected function _logImportResult()
974     {
975         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
976             . ' Import finished. (total: ' . $this->_importResult['totalcount'] 
977             . ' fail: ' . $this->_importResult['failcount'] 
978             . ' duplicates: ' . $this->_importResult['duplicatecount'] 
979             . ' updates: ' . $this->_importResult['updatecount'] 
980             . ')');
981     }
982     
983     /**
984      * returns config from definition
985      * 
986      * @param Tinebase_Model_ImportExportDefinition $_definition
987      * @param array                                 $_options
988      * @return array
989      */
990     public static function getOptionsArrayFromDefinition($_definition, $_options)
991     {
992         $options = Tinebase_ImportExportDefinition::getOptionsAsZendConfigXml($_definition, $_options);
993         $optionsArray = $options->toArray();
994         if (! isset($optionsArray['model'])) {
995             $optionsArray['model'] = $_definition->model;
996         }
997         
998         return $optionsArray;
999     }
1000     
1001     /**
1002      * set controller
1003      */
1004     protected function _setController()
1005     {
1006         $this->_controller = Tinebase_Core::getApplicationInstance($this->_options['model']);
1007     }
1008 }