e2032ea277546a05c5fb854b939a3a2b5771fc54
[kivitendo-erp.git] / SL / Helper / Csv.pm
1 package SL::Helper::Csv;
2
3 use strict;
4 use warnings;
5
6 use Carp;
7 use IO::File;
8 use Params::Validate qw(:all);
9 use Text::CSV_XS;
10 use Rose::Object::MakeMethods::Generic scalar => [ qw(
11   file encoding sep_char quote_char escape_char header profile class
12   numberformat dateformat ignore_unknown_columns strict_profile _io _csv
13   _objects _parsed _data _errors
14 ) ];
15
16 use SL::Helper::Csv::Dispatcher;
17 use SL::Helper::Csv::Error;
18
19 # public interface
20
21 sub new {
22   my $class  = shift;
23   my %params = validate(@_, {
24     sep_char               => { default => ';' },
25     quote_char             => { default => '"' },
26     escape_char            => { default => '"' },
27     header                 => { type    => ARRAYREF, optional => 1 },
28     profile                => { type    => HASHREF,  optional => 1 },
29     file                   => 1,
30     encoding               => 0,
31     class                  => 0,
32     numberformat           => 0,
33     dateformat             => 0,
34     ignore_unknown_columns => 0,
35     strict_profile         => 0,
36   });
37   my $self = bless {}, $class;
38
39   $self->$_($params{$_}) for keys %params;
40
41   $self->_io(IO::File->new);
42   $self->_csv(Text::CSV_XS->new({
43     binary => 1,
44     sep_char    => $self->sep_char,
45     quote_char  => $self->quote_char,
46     escape_char => $self->escape_char,
47
48   }));
49   $self->_errors([]);
50
51   return $self;
52 }
53
54 sub parse {
55   my ($self, %params) = @_;
56
57   $self->_open_file;
58   return if ! $self->_check_header;
59   return if ! $self->dispatcher->parse_profile;
60   return if ! $self->_parse_data;
61
62   $self->_parsed(1);
63   return $self;
64 }
65
66 sub get_data {
67   $_[0]->_data;
68 }
69
70 sub get_objects {
71   my ($self, %params) = @_;
72   croak 'no class given'   unless $self->class;
73   croak 'must parse first' unless $self->_parsed;
74
75   $self->_make_objects unless $self->_objects;
76   return wantarray ? @{ $self->_objects } : $self->_objects;
77 }
78
79 sub errors {
80   @{ $_[0]->_errors }
81 }
82
83 sub check_header {
84   $_[0]->_check_header;
85 }
86
87 # private stuff
88
89 sub _open_file {
90   my ($self, %params) = @_;
91
92   $self->encoding($self->_guess_encoding) if !$self->encoding;
93
94   $self->_io->open($self->file, '<' . $self->_encode_layer)
95     or die "could not open file " . $self->file;
96
97   return $self->_io;
98 }
99
100 sub _check_header {
101   my ($self, %params) = @_;
102   return $self->header if $self->header;
103
104   my $header = $self->_csv->getline($self->_io);
105
106   $self->_push_error([
107     $self->_csv->error_input,
108     $self->_csv->error_diag,
109     0,
110   ]) unless $header;
111
112   $self->header($header);
113 }
114
115 sub _parse_data {
116   my ($self, %params) = @_;
117   my (@data, @errors);
118
119   $self->_csv->column_names(@{ $self->header });
120
121   while (1) {
122     my $row = $self->_csv->getline($self->_io);
123     if ($row) {
124       my %hr;
125       @hr{@{ $self->header }} = @$row;
126       push @data, \%hr;
127     } else {
128       last if $self->_csv->eof;
129       push @errors, [
130         $self->_csv->error_input,
131         $self->_csv->error_diag,
132         $self->_io->input_line_number,
133       ];
134     }
135     last if $self->_csv->eof;
136   }
137
138   $self->_data(\@data);
139   $self->_push_error(@errors);
140
141   return ! @errors;
142 }
143
144 sub _encode_layer {
145   ':encoding(' . $_[0]->encoding . ')';
146 }
147
148 sub _make_objects {
149   my ($self, %params) = @_;
150   my @objs;
151
152   eval "require " . $self->class;
153   local $::myconfig{numberformat} = $self->numberformat if $self->numberformat;
154   local $::myconfig{dateformat}   = $self->dateformat   if $self->dateformat;
155
156   for my $line (@{ $self->_data }) {
157     my $tmp_obj = $self->class->new;
158     $self->dispatcher->dispatch($tmp_obj, $line);
159     push @objs, $tmp_obj;
160   }
161
162   $self->_objects(\@objs);
163 }
164
165 sub dispatcher {
166   my ($self, %params) = @_;
167
168   $self->{_dispatcher} ||= $self->_make_dispatcher;
169 }
170
171 sub _make_dispatcher {
172   my ($self, %params) = @_;
173
174   die 'need a header to make a dispatcher' unless $self->header;
175
176   return SL::Helper::Csv::Dispatcher->new($self);
177 }
178
179 sub _guess_encoding {
180   # won't fix
181   'utf-8';
182 }
183
184 sub _push_error {
185   my ($self, @errors) = @_;
186   my @new_errors = ($self->errors, map { SL::Helper::Csv::Error->new(@$_) } @errors);
187   $self->_errors(\@new_errors);
188 }
189
190
191 1;
192
193 __END__
194
195 =encoding utf-8
196
197 =head1 NAME
198
199 SL::Helper::Csv - take care of csv file uploads
200
201 =head1 SYNOPSIS
202
203   use SL::Helper::Csv;
204
205   my $csv = SL::Helper::Csv->new(
206     file        => \$::form->{upload_file},
207     encoding    => 'utf-8', # undef means utf8
208     sep_char    => ',',     # default ';'
209     quote_char  => '\'',    # default '"'
210     escape_char => '"',     # default '"'
211     header      => [qw(id text sellprice word)], # see later
212     profile     => { sellprice => 'sellprice_as_number' },
213     class       => 'SL::DB::CsvLine',   # if present, map lines to this
214   );
215
216   my $status  = $csv->parse;
217   my $hrefs   = $csv->get_data;
218   my @objects = $csv->get_objects;
219
220   my @errors  = $csv->errors;
221
222 =head1 DESCRIPTION
223
224 See Synopsis.
225
226 Text::CSV offeres already good functions to get lines out of a csv file, but in
227 most cases you will want those line to be parsed into hashes or even objects,
228 so this model just skips ahead and gives you objects.
229
230 Its basic assumptions are:
231
232 =over 4
233
234 =item You do know what you expect to be in that csv file.
235
236 This means first and foremost you have knowledge about encoding, number and
237 date format, csv parameters such as quoting and separation characters. You also
238 know what content will be in that csv and what L<Rose::DB> is responsible for
239 it. You provide valid header columns and their mapping to the objects.
240
241 =item You do NOT know if the csv provider yields to your expectations.
242
243 Stuff that does not work with what you expect should not crash anything, but
244 give you a hint what went wrong. As a result, if you remeber to check for
245 errors after each step, you should be fine.
246
247 =item Data does not make sense. It's just data.
248
249 Almost all data imports have some type of constraints. Some data needs to be
250 unique, other data needs to be connected to existing data sets. This will not
251 happen here. You will receive a plain mapping of the data into the class tree,
252 nothing more.
253
254 =back
255
256 =head1 METHODS
257
258 =over 4
259
260 =item C<new> PARAMS
261
262 Standard constructor. You can use this to set most of the data.
263
264 =item C<parse>
265
266 Do the actual work. Will return true ($self actually) if success, undef if not.
267
268 =item C<get_objects>
269
270 Parse the data into objects and return those.
271
272 This method will return list or arrayref depending on context.
273
274 =item C<get_data>
275
276 Returns an arrayref of the raw lines as hashrefs.
277
278 =item C<errors>
279
280 Return all errors that came up during parsing. See error handling for detailed
281 information.
282
283 =back
284
285 =head1 PARAMS
286
287 =over 4
288
289 =item C<file>
290
291 The file which contents are to be read. Can be a name of a physical file or a
292 scalar ref for memory data.
293
294 =item C<encoding>
295
296 Encoding of the CSV file. Note that this module does not do any encoding
297 guessing. Know what your data is. Defaults to utf-8.
298
299 =item C<sep_char>
300
301 =item C<quote_char>
302
303 =item C<escape_char>
304
305 Same as in L<Text::CSV>
306
307 =item C<header> \@FIELDS
308
309 Can be an array of columns, in this case the first line is not used as a
310 header. Empty header fields will be ignored in objects.
311
312 =item C<profile> \%ACCESSORS
313
314 May be used to map header fields to custom accessors. Example:
315
316   { listprice => listprice_as_number }
317
318 In this case C<listprice_as_number> will be used to read in values from the
319 C<listprice> column.
320
321 In case of a One-To-One relationsship these can also be set over
322 relationsships by sparating the steps with a dot (C<.>). This will work:
323
324   { customer => 'customer.name' }
325
326 And will result in something like this:
327
328   $obj->customer($obj->meta->relationship('customer')->class->new);
329   $obj->customer->name($csv_line->{customer})
330
331 But beware, this will not try to look up anything in the database. You will
332 simply receive objects that represent what the profile defined. If some of
333 these information are unique, and should be connected to preexisting data, you
334 will have to do that for yourself. Since you provided the profile, it is
335 assumed you know what to do in this case.
336
337 =item C<class>
338
339 If present, the line will be handed to the new sub of this class,
340 and the return value used instead of the line itself.
341
342 =item C<ignore_unknown_columns>
343
344 If set, the import will ignore unkown header columns. Useful for lazy imports,
345 but deactivated by default.
346
347 =item C<strict_profile>
348
349 If set, all columns to be parsed must be specified in C<profile>. Every header
350 field not listed there will be treated like an unknown column.
351
352 =back
353
354 =head1 ERROR HANDLING
355
356 After parsing a file all errors will be accumulated into C<errors>.
357 Each entry is an object with the following attributes:
358
359  raw_input:  offending raw input,
360  code:   Text::CSV error code if Text:CSV signalled an error, 0 else,
361  diag:   error diagnostics,
362  line:   position in line,
363  col:    estimated line in file,
364
365 Note that the last entry can be off, but will give an estimate.
366
367 =head1 CAVEATS
368
369 =over 4
370
371 =item *
372
373 sep_char, quote_char, and escape_char are passed to Text::CSV on creation.
374 Changing them later has no effect currently.
375
376 =item *
377
378 Encoding errors are not dealt with properly.
379
380 =back
381
382 =head1 TODO
383
384 Dispatch to child objects, like this:
385
386  $csv = SL::Helper::Csv->new(
387    file  => ...
388    class => SL::DB::Part,
389    profile => [
390      makemodel => {
391        make_1  => make,
392        model_1 => model,
393      },
394      makemodel => {
395        make_2  => make,
396        model_2 => model,
397      },
398    ]
399  );
400
401 =head1 AUTHOR
402
403 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
404
405 =cut