Trim whitespace in combined import fields
[tine20] / tine20 / Tinebase / Import / Csv / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @author      Philipp Schüle <p.schuele@metaways.de>
8  * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
9  */
10
11 /**
12  * abstract csv import class
13  * 
14  * some documentation for the xml import definition:
15  * 
16  * <delimiter>TAB</delimiter>:           use tab as delimiter
17  * <config> main tags
18  * <container_id>34</container_id>:     container id for imported records (required)
19  * <encoding>UTF-8</encoding>:          encoding of input file
20  * <duplicates>1<duplicates>:           check for duplicates
21  * <use_headline>0</use_headline>:      just remove the headline/first line but do not use it for mapping
22  *
23  * <mapping><field> special tags:
24  * <append>glue</append>:               value is appended to destination field with 'glue' as glue
25  * <replace>\n</replace>:               replace \r\n with \n
26  * <fixed>fixed</fixed>:                the field has a fixed value ('fixed' in this example)
27  * 
28  *
29  * @todo        add tests for notes
30  * @todo        add more documentation
31  * @package     Tinebase
32  * @subpackage  Import
33  */
34 abstract class Tinebase_Import_Csv_Abstract extends Tinebase_Import_Abstract
35 {
36     /**
37      * csv headline
38      * 
39      * @var array
40      */
41     protected $_headline = array();
42     
43     /**
44      * special delimiters
45      * 
46      * @var array
47      */
48     protected $_specialDelimiter = array(
49         'TAB'   => "\t"
50     );
51     
52     /**
53      * constructs a new importer from given config
54      * 
55      * @param array $_options
56      * @throws Tinebase_Exception_InvalidArgument
57      */
58     public function __construct(array $_options = array())
59     {
60         $this->_options = array_merge($this->_options, array(
61             'maxLineLength'               => 8000,
62             'delimiter'                   => ',',
63             'enclosure'                   => '"',
64             'escape'                      => '\\',
65             'encodingTo'                  => 'UTF-8',
66             'mapping'                     => '',
67             'headline'                    => 0,
68             'use_headline'                => 1,
69             'mapUndefinedFieldsEnable'    => 0,
70             'mapUndefinedFieldsTo'        => 'description'
71         ));
72         
73         parent::__construct($_options);
74         
75         if (empty($this->_options['model'])) {
76             throw new Tinebase_Exception_InvalidArgument(get_class($this) . ' needs model in config.');
77         }
78         
79         $this->_setController();
80     }
81
82     /**
83      * get raw data of a single record
84      * 
85      * @param  resource $_resource
86      * @return array
87      */
88     protected function _getRawData($_resource)
89     {
90         $delimiter = ((isset($this->_specialDelimiter[$this->_options['delimiter']]) || array_key_exists($this->_options['delimiter'], $this->_specialDelimiter))) ? $this->_specialDelimiter[$this->_options['delimiter']] : $this->_options['delimiter'];
91         $lineData = fgetcsv(
92             $_resource,
93             $this->_options['maxLineLength'],
94             $delimiter,
95             $this->_options['enclosure']
96             // escape param is only available in PHP >= 5.3.0
97             // $this->_options['escape']
98         );
99         
100         if (is_array($lineData) && count($lineData) == 1) {
101             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Only got 1 field in line. Wrong delimiter?');
102         }
103         
104         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
105             . ' Raw data: ' . print_r($lineData, true));
106         
107         return $lineData;
108     }
109     
110     /**
111      * do something before the import
112      * 
113      * @param resource $_resource
114      */
115     protected function _beforeImport($_resource = NULL)
116     {
117         // get headline
118         if (isset($this->_options['headline']) && $this->_options['headline']) {
119             $this->_headline = $this->_getRawData($_resource);
120             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
121                 . ' Got headline: ' . implode(', ', $this->_headline));
122             if (! $this->_options['use_headline']) {
123                 // just read headline but do not use it
124                 $this->_headline = array();
125             } else {
126                 array_walk($this->_headline, function(&$value) {
127                     $value = trim($value);
128                 });
129             }
130         }
131     }
132     
133     /**
134      * do the mapping
135      *
136      * @param array $_data
137      * @return array
138      * @throws Tinebase_Exception_UnexpectedValue
139      */
140     protected function _doMapping($_data)
141     {
142         $data = array();
143         $_data_indexed = array();
144         
145         if (! empty($this->_headline) && sizeof($this->_headline) == sizeof($_data)) {
146             $_data_indexed = array_combine($this->_headline, $_data);
147         }
148
149         if (! isset($this->_options['mapping']['field']) || ! is_array($this->_options['mapping']['field'])) {
150             throw new Tinebase_Exception_UnexpectedValue('No field mapping defined');
151         }
152
153         $this->_mapValuesToDestination($_data_indexed, $_data, $data);
154
155         if ($this->_options['mapUndefinedFieldsEnable'] == 1) {
156             $undefinedFieldsText = $this->_createInfoTextForUnmappedFields($_data_indexed);
157             if (! $undefinedFieldsText === false) {
158                 if ((isset($data[$this->_options['mapUndefinedFieldsTo']]) || array_key_exists($this->_options['mapUndefinedFieldsTo'], $data))) {
159                     $data[$this->_options['mapUndefinedFieldsTo']] .= $this->_createInfoTextForUnmappedFields($_data_indexed);
160                 } else {
161                     $data[$this->_options['mapUndefinedFieldsTo']] = $this->_createInfoTextForUnmappedFields($_data_indexed);
162                 }
163             }
164         }
165         
166         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
167             . ' Mapped data: ' . print_r($data, true));
168         
169         return $data;
170     }
171
172     /**
173      * map values to destination fields
174      *
175      * @param $_data_indexed
176      * @param $_data
177      * @param $data
178      */
179     protected function _mapValuesToDestination($_data_indexed, $_data, &$data)
180     {
181         foreach ($this->_options['mapping']['field'] as $index => $field) {
182             if (empty($_data_indexed) && isset($_data[$index])) {
183                 $value = $_data[$index];
184             } else if (isset($field['source']) && isset($_data_indexed[$field['source']])) {
185                 $value = $_data_indexed[$field['source']];
186             } else {
187                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
188                     . ' No value found for field ' . (isset($field['source']) ? $field['source'] : print_r($field, true)));
189                 continue;
190             }
191
192             if ((! isset($field['destination']) || empty($field['destination'])) && ! isset($field['destinations'])) {
193                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
194                     . ' No destination in definition for field ' . $field['source']);
195                 continue;
196             }
197
198             if (isset($field['destinations']) && isset($field['destinations']['destination'])) {
199                 $destinations = $field['destinations']['destination'];
200                 $delimiter = isset($field['$separator']) && ! empty($field['$separator']) ? $field['$separator'] : ' ';
201                 $values = array_map('trim', explode($delimiter, $value, count($destinations)));
202                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
203                     . ' values: ' . print_r($values, true));
204                 $i = 0;
205                 foreach ($destinations as $destination) {
206                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
207                         . ' destination ' . $destination);
208                     $data[$destination] = $values[$i++];
209                 }
210             } else {
211                 $data[$field['destination']] = $value;
212             }
213         }
214     }
215     
216     /**
217      * Generates a text with every undefined data from import 
218      * 
219      * @param array $_data_indexed
220      * @return string
221      */
222     protected function _createInfoTextForUnmappedFields ($_data_indexed)
223     {
224         $return = null;
225         
226         $translation = Tinebase_Translation::getTranslation('Tinebase');
227         
228         $validKeys = array();
229         foreach ($this->_options['mapping']['field'] as $keys) {
230             $validKeys[$keys['source']] = null;
231         }
232         // This is an array containing every not mapped field as key with his value.
233         $notImportedFields = array_diff_key($_data_indexed, $validKeys);
234         
235         if (count($notImportedFields) >= 1) {
236             $description = sprintf($translation->_("The following fields weren't imported: %s"), "\n");
237             $valueIfEmpty = $translation->_("N/A");
238             
239             foreach ($notImportedFields as $nKey => $nVal) {
240                 if (trim($nKey) == "") $nKey = $valueIfEmpty;
241                 if (trim($nVal) == "") $nVal = $valueIfEmpty;
242                 
243                 $description .= $nKey . " : " . $nVal . " \n";
244             }
245             $return = $description;
246         } else {
247             $return = false;
248         }
249         return $return;
250     }
251 }