]> wagnertech.de Git - mfinanz.git/blob - SL/Helper/Csv.pm
restart apache2 in postinst
[mfinanz.git] / SL / Helper / Csv.pm
1 package SL::Helper::Csv;
2
3 use strict;
4 use warnings;
5
6 use version 0.77;
7 use Carp;
8 use IO::File;
9 use Params::Validate qw(:all);
10 use List::MoreUtils qw(all pairwise firstidx);
11 use Text::CSV_XS;
12 use Rose::Object::MakeMethods::Generic scalar => [ qw(
13   file encoding sep_char quote_char escape_char header profile
14   numberformat dateformat ignore_unknown_columns strict_profile is_multiplexed
15   _row_header _io _csv _objects _parsed _data _errors all_cvar_configs case_insensitive_header
16   _multiplex_datatype_position
17 ) ];
18
19 use SL::Helper::Csv::Dispatcher;
20 use SL::Helper::Csv::Error;
21
22 # public interface
23
24 sub new {
25   my $class  = shift;
26   my %params = validate(@_, {
27     sep_char               => { default => ';' },
28     quote_char             => { default => '"' },
29     escape_char            => { default => '"' },
30     header                 => { type    => ARRAYREF, optional => 1 },
31     profile                => { type    => ARRAYREF, optional => 1 },
32     file                   => 1,
33     encoding               => 0,
34     numberformat           => 0,
35     dateformat             => 0,
36     ignore_unknown_columns => 0,
37     strict_profile         => 0,
38     case_insensitive_header => 0,
39   });
40   my $self = bless {}, $class;
41
42   $self->$_($params{$_}) for keys %params;
43
44   $self->_io(IO::File->new);
45   $self->_csv(Text::CSV_XS->new({
46     binary => 1,
47     sep_char    => $self->sep_char,
48     quote_char  => $self->quote_char,
49     escape_char => $self->escape_char,
50
51   }));
52   $self->_errors([]);
53
54   return $self;
55 }
56
57 sub parse {
58   my ($self, %params) = @_;
59
60   $self->_open_file;
61   return if ! $self->_check_multiplexed;
62   return if ! $self->_check_header;
63   return if ! $self->_check_multiplex_datatype_position;
64   return if ! $self->dispatcher->parse_profile;
65   return if ! $self->_parse_data;
66
67   $self->_parsed(1);
68   return $self;
69 }
70
71 sub get_data {
72   $_[0]->_data;
73 }
74
75 sub get_objects {
76   my ($self, %params) = @_;
77   croak 'must parse first' unless $self->_parsed;
78
79   $self->_make_objects unless $self->_objects;
80   return $self->_objects;
81 }
82
83 sub errors {
84   @{ $_[0]->_errors }
85 }
86
87 sub check_header {
88   $_[0]->_check_header;
89 }
90
91 # private stuff
92
93 sub _open_file {
94   my ($self, %params) = @_;
95
96   $self->encoding($self->_guess_encoding) if !$self->encoding;
97
98   $self->_io->open($self->file, '<' . $self->_encode_layer)
99     or die "could not open file " . $self->file;
100
101   return $self->_io;
102 }
103
104 # check, if data is multiplexed and if all nessesary infos are given
105 sub _check_multiplexed {
106   my ($self, %params) = @_;
107
108   $self->is_multiplexed(0);
109
110   # If more than one profile is given, it is multiplexed.
111   if ($self->profile) {
112     my @profile = @{ $self->profile };
113     if (scalar @profile > 1) {
114       # Each profile needs a class and a row_ident
115       my $info_ok = all { defined $_->{class} && defined $_->{row_ident} } @profile;
116       $self->_push_error([
117         undef,
118         0,
119         "missing class or row_ident in one of the profiles for multiplexed data",
120         0,
121         0]) unless $info_ok;
122
123       # If header is given, there needs to be a header for each profile
124       # and no empty headers.
125       if ($info_ok && $self->header) {
126         my @header = @{ $self->header };
127         my $t_ok = scalar @profile == scalar @header;
128         $self->_push_error([
129           undef,
130           0,
131           "number of headers and number of profiles must be the same for multiplexed data",
132           0,
133           0]) unless $t_ok;
134         $info_ok = $info_ok && $t_ok;
135
136         $t_ok = all { scalar @$_ > 0} @header;
137         $self->_push_error([
138           undef,
139           0,
140           "no empty headers are allowed for multiplexed data",
141           0,
142           0]) unless $t_ok;
143         $info_ok = $info_ok && $t_ok;
144       }
145       $self->is_multiplexed($info_ok);
146       return $info_ok;
147     }
148   }
149
150   # ok, if not multiplexed
151   return 1;
152 }
153
154 sub _check_header {
155   my ($self, %params) = @_;
156   my $header;
157
158   $header = $self->header;
159   if (!$header) {
160     my $n_header = ($self->is_multiplexed)? scalar @{ $self->profile } : 1;
161     foreach my $p_num (0..$n_header - 1) {
162       my $h = $self->_csv->getline($self->_io);
163
164       my ($code, $string, $position, $record, $field) = $self->_csv->error_diag;
165
166       $self->_push_error([
167         $self->_csv->error_input,
168         $code, $string, $position, $record // 0,
169       ]) unless $h;
170
171       if ($self->is_multiplexed) {
172         push @{ $header }, $h;
173       } else {
174         $header = $h;
175       }
176     }
177   }
178
179   # Special case: utf8 BOM.
180   # certain software (namely MS Office and notepad.exe insist on prefixing
181   # data with a discouraged but valid byte order mark
182   # if not removed, the first header field will not be recognized
183   if ($header) {
184     my $h = ($self->is_multiplexed)? $header->[0] : $header;
185
186     if ($h && $h->[0] && $self->encoding =~ /utf-?8/i) {
187       $h->[0] =~ s/^\x{FEFF}//;
188     }
189   }
190
191   # check, if all header fields are parsed well
192   if ($self->is_multiplexed) {
193     return unless $header && all { $_ } @$header;
194   } else {
195     return unless $header;
196   }
197
198   # Special case: human stupidity
199   # people insist that case sensitivity doesn't exist and try to enter all
200   # sorts of stuff. at this point we've got a profile (with keys that represent
201   # valid methods), and a header full of strings. if two of them match, the user
202   # most likely meant that field, so rewrite the header
203   if ($self->case_insensitive_header) {
204     die 'case_insensitive_header is only possible with profile' unless $self->profile;
205     if ($header) {
206       my $h_aref = ($self->is_multiplexed)? $header : [ $header ];
207       my $p_num  = 0;
208       foreach my $h (@{ $h_aref }) {
209         my %names = (
210           (map { $_ => $_                                     } keys %{ $self->profile->[$p_num]->{profile} || {} }),
211           (map { $_ => $self->profile->[$p_num]{mapping}{$_}  } keys %{ $self->profile->[$p_num]->{mapping} || {} }),
212         );
213         for my $name (keys %names) {
214           for my $i (0..$#$h) {
215             $h->[$i] = $names{$name} if lc $h->[$i] eq lc $name;
216           }
217         }
218         $p_num++;
219       }
220     }
221   }
222
223   return $self->header($header);
224 }
225
226 sub _check_multiplex_datatype_position {
227   my ($self) = @_;
228
229   return 1 if !$self->is_multiplexed; # ok if not multiplexed
230
231   my @positions = map { firstidx { 'datatype' eq lc($_) } @{ $_ } } @{ $self->header };
232   my $first_pos = $positions[0];
233   if (all { $first_pos == $_ } @positions) {
234     $self->_multiplex_datatype_position($first_pos);
235     return 1;
236   } else {
237     $self->_push_error([undef,
238                         0,
239                         "datatype field must be at the same position for all datatypes for multiplexed data",
240                         0,
241                         0]);
242     return 0;
243   }
244 }
245
246 sub _is_empty_row {
247   return !!all { !$_ } @{$_[0]};
248 }
249
250 sub _parse_data {
251   my ($self, %params) = @_;
252   my (@data, @errors);
253
254   while (1) {
255     my $row = $self->_csv->getline($self->_io);
256     if ($row) {
257       next if _is_empty_row($row);
258       my $header = $self->_header_by_row($row);
259       if (!$header) {
260         push @errors, [
261           undef,
262           0,
263           "Cannot get header for row. Maybe row name and datatype field not matching.",
264           0,
265           0];
266         last;
267       }
268       my %hr;
269       @hr{@{ $header }} = @$row;
270       push @data, \%hr;
271     } else {
272       last if $self->_csv->eof;
273
274       # Text::CSV_XS 0.89 added record number to error_diag
275       my ($code, $string, $position, $record, $field) = $self->_csv->error_diag;
276
277       push @errors, [
278         $self->_csv->error_input,
279         $code, $string, $position,
280         $record // $self->_io->input_line_number,
281       ];
282     }
283     last if $self->_csv->eof;
284   }
285
286   $self->_data(\@data);
287   $self->_push_error(@errors);
288
289   return ! @errors;
290 }
291
292 sub _header_by_row {
293   my ($self, $row) = @_;
294
295   # initialize lookup hash if not already done
296   if ($self->is_multiplexed && ! defined $self->_row_header ) {
297     $self->_row_header({ pairwise { no warnings 'once'; $a->{row_ident} => $b } @{ $self->profile }, @{ $self->header } });
298   }
299
300   if ($self->is_multiplexed) {
301     return $self->_row_header->{$row->[$self->_multiplex_datatype_position]}
302   } else {
303     return $self->header;
304   }
305 }
306
307 sub _encode_layer {
308   ':encoding(' . $_[0]->encoding . ')';
309 }
310
311 sub _make_objects {
312   my ($self, %params) = @_;
313   my @objs;
314
315   local $::myconfig{numberformat} = $self->numberformat if $self->numberformat;
316   local $::myconfig{dateformat}   = $self->dateformat   if $self->dateformat;
317
318   my $line_nr = 0;
319   for my $line (@{ $self->_data }) {
320     $line_nr++;
321     eval {
322       my $tmp_obj = $self->dispatcher->dispatch($line);
323       push @objs, $tmp_obj;
324       1;
325     } or do {
326       $self->_push_error([
327         $line,
328         0,
329         "making objects failed: $@",
330         0,
331         $line_nr]);
332     };
333   }
334
335   $self->_objects(\@objs);
336 }
337
338 sub dispatcher {
339   my ($self, %params) = @_;
340
341   $self->{_dispatcher} ||= $self->_make_dispatcher;
342 }
343
344 sub _make_dispatcher {
345   my ($self, %params) = @_;
346
347   die 'need a header to make a dispatcher' unless $self->header;
348
349   return SL::Helper::Csv::Dispatcher->new($self);
350 }
351
352 sub _guess_encoding {
353   # won't fix
354   'utf-8';
355 }
356
357 sub _push_error {
358   my ($self, @errors) = @_;
359   my @new_errors = ($self->errors, map { SL::Helper::Csv::Error->new(@$_) } @errors);
360   $self->_errors(\@new_errors);
361 }
362
363 sub specs {
364   $_[0]->dispatcher->_specs
365 }
366
367 1;
368
369 __END__
370
371 =encoding utf-8
372
373 =head1 NAME
374
375 SL::Helper::Csv - take care of csv file uploads
376
377 =head1 SYNOPSIS
378
379   use SL::Helper::Csv;
380
381   my $csv = SL::Helper::Csv->new(
382     file        => \$::form->{upload_file},
383     encoding    => 'utf-8', # undef means utf8
384     sep_char    => ',',     # default ';'
385     quote_char  => '\'',    # default '"'
386     escape_char => '"',     # default '"'
387     header      => [ qw(id text sellprice word) ], # see later
388     profile     => [ { profile => { sellprice => 'sellprice_as_number'},
389                        class   => 'SL::DB::Part' } ],
390   );
391
392   my $status  = $csv->parse;
393   my $hrefs   = $csv->get_data;
394   my $objects = $csv->get_objects;
395
396   my @errors  = $csv->errors;
397
398 =head1 DESCRIPTION
399
400 See Synopsis.
401
402 Text::CSV already offers good functions to get lines out of a csv file, but in
403 most cases you will want those lines to be parsed into hashes or even objects,
404 so this model just skips ahead and gives you objects.
405
406 Its basic assumptions are:
407
408 =over 4
409
410 =item You do know what you expect to be in that csv file.
411
412 This means first and foremost that you have knowledge about encoding, number and
413 date format, csv parameters such as quoting and separation characters. You also
414 know what content will be in that csv and what L<Rose::DB> is responsible for
415 it. You provide valid header columns and their mapping to the objects.
416
417 =item You do NOT know if the csv provider yields to your expectations.
418
419 Stuff that does not work with what you expect should not crash anything, but
420 give you a hint what went wrong. As a result, if you remember to check for
421 errors after each step, you should be fine.
422
423 =item Data does not make sense. It's just data.
424
425 Almost all data imports have some type of constraints. Some data needs to be
426 unique, other data needs to be connected to existing data sets. This will not
427 happen here. You will receive a plain mapping of the data into the class tree,
428 nothing more.
429
430 =item Multiplex data
431
432 This module can handle multiplexed data of different class types. In that case
433 multiple profiles with classes and row identifiers must be given. Multiple
434 headers may also be given or read from csv data. Data must contain the row
435 identifier in the column named 'datatype'.
436
437 =back
438
439 =head1 METHODS
440
441 =over 4
442
443 =item C<new> PARAMS
444
445 Standard constructor. You can use this to set most of the data.
446
447 =item C<parse>
448
449 Do the actual work. Will return true ($self actually) if success, undef if not.
450
451 =item C<get_objects>
452
453 Parse the data into objects and return those.
454
455 This method will return an arrayref of all objects.
456
457 If an error occurs while making the objects, it is put errors, too.
458
459 =item C<get_data>
460
461 Returns an arrayref of the raw lines as hashrefs.
462
463 =item C<errors>
464
465 Return all errors that came up during parsing or making objects.
466 See error handling for detailed information.
467
468 =back
469
470 =head1 PARAMS
471
472 =over 4
473
474 =item C<file>
475
476 The file which contents are to be read. Can be a name of a physical file or a
477 scalar ref for memory data.
478
479 =item C<encoding>
480
481 Encoding of the CSV file. Note that this module does not do any encoding
482 guessing. Know what your data is. Defaults to utf-8.
483
484 =item C<sep_char>
485
486 =item C<quote_char>
487
488 =item C<escape_char>
489
490 Same as in L<Text::CSV>
491
492 =item C<header> \@HEADERS
493
494 If given, it contains an ARRAY of the header fields for not multiplexed data.
495 Or an ARRAYREF for each different class type for multiplexed data. These
496 ARRAYREFS are the header fields which are an array of columns. In this case
497 the first lines are not used as a header. Empty header fields will be ignored
498 in objects.
499
500 If not given, headers are taken from the first n lines of data, where n is the
501 number of different class types.
502
503 In case of multiplexed data there must be a column named 'datatype'. This
504 column must be given in each header and must be at the same position in each
505 header.
506
507 Examples:
508
509   classic data of one type:
510   [ 'name', 'street', 'zipcode', 'city' ]
511
512   multiplexed data with two different types:
513   [ [ 'datatype', 'ordernumber', 'customer', 'transdate' ],
514     [ 'datatype', 'partnumber', 'qty', 'sellprice' ] ]
515
516 =item C<profile> PROFILE_DATA
517
518 The profile mapping csv to the objects.
519
520 See section L</PROFILE> for information on this topic.
521
522 =item C<ignore_unknown_columns>
523
524 If set, the import will ignore unknown header columns. Useful for lazy imports,
525 but deactivated by default.
526
527 =item C<case_insensitive_header>
528
529 If set, header columns will be matched against profile entries case
530 insensitive, and on match the profile name will be taken.
531
532 Only works if a profile is given, will die otherwise.
533
534 If both C<case_insensitive_header> and C<strict_profile> is set, matched header
535 columns will be accepted.
536
537 =item C<strict_profile>
538
539 If set, all columns to be parsed must be specified in C<profile>. Every header
540 field not listed there will be treated like an unknown column.
541
542 If both C<case_insensitive_header> and C<strict_profile> is set, matched header
543 columns will be accepted.
544
545 =back
546
547 =head1 PROFILE
548
549 The profile is needed for mapping csv data to the accessors in the data object.
550
551 The basic structure is:
552
553   PROFILE       := [ CLASS_PROFILE, CLASS_PROFILE* ]
554   CLASS_PROFILE := {
555                       profile   => { ACCESSORS+ },
556                       class     => $classname,
557                       row_ident => $row_ident,
558                       mapping   => { MAPPINGS* },
559                    }
560   ACCESSORS     := $field => $accessor
561   MAPPINGS      := $alias => $field
562
563 The C<ACCESSORS> may be used to map header fields to custom
564 accessors. Example:
565
566   profile => {
567     listprice => 'listprice_as_number',
568   }
569
570 In this case C<listprice_as_number> will be used to store the values from the
571 C<listprice> column.
572
573 In case of a One-To-One relationship these can also be set over
574 relationships by separating the steps with a dot (C<.>). This will work:
575
576   customer => 'customer.name',
577
578 And will result in something like this:
579
580   $obj->customer($obj->meta->relationship('customer')->class->new);
581   $obj->customer->name($csv_line->{customer})
582
583 Beware, this will not try to look up anything in the database! You will
584 simply receive objects that represent what the profile defined. If some of
585 these information are unique, or should be connected to preexisting data, you
586 will have to do that for yourself. Since you provided the profile, it is
587 assumed you know what to do in this case.
588
589 If no profile is given, any header field found will be taken as is.
590
591 If the path in a profile entry is empty, the field will be subjected to
592 C<strict_profile> and C<case_insensitive_header> checking and will be parsed
593 into C<get_data>, but will not be attempted to be dispatched into objects.
594
595 C<class> must be present. A new instance will be created for each line before
596 dispatching into it.
597
598 C<row_ident> is used to determine the correct profile in multiplexed data and
599 must be given there. It's not used in non-multiplexed data.
600
601 If C<mappings> is present, it must contain a hashref that maps strings to known
602 fields. This can be used to add custom profiles for known sources, that don't
603 comply with the expected header identities.
604
605 Without strict profiles, mappings can also directly map header fields that
606 should end up in the same accessor.
607
608 With case insensitive headings, mappings will also modify the headers, to fit
609 the expected profile.
610
611 Mappings can be identical to known fields and will be prefered during lookup,
612 but will not replace the field, meaning that:
613
614   profile => {
615     name        => 'name',
616     description => 'description',
617   }
618   mapping => {
619     name        => 'description',
620     shortname   => 'name',
621   }
622
623 will work as expected, and shortname will not end up in description. This also
624 works with the case insensitive option. Note however that the case insensitive
625 option will not enable true unicode collating.
626
627
628 Here's a full example:
629
630   [
631     {
632       class     => 'SL::DB::Order',
633       row_ident => 'O'
634     },
635     {
636       class     => 'SL::DB::OrderItem',
637       row_ident => 'I',
638       profile   => { sellprice => 'sellprice_as_number' },
639       mapping   => { 'Verkaufspreis' => 'sellprice' }
640     },
641   ]
642
643 =head1 ERROR HANDLING
644
645 After parsing a file all errors will be accumulated into C<errors>.
646 Each entry is an object with the following attributes:
647
648  raw_input:  offending raw input,
649  code:   Text::CSV error code if Text:CSV signalled an error, 0 else,
650  diag:   error diagnostics,
651  line:   position in line,
652  col:    estimated line in file,
653
654 Note that the last entry can be off, but will give an estimate.
655
656 Error handling is also known to break on new Perl versions and need to be
657 adjusted from time to time due to changes in Text::CSV_XS.
658
659 If an error occurs while making the objects, it is put into errors, too.
660 Here the line is also an estimation.
661
662 =head1 CAVEATS
663
664 =over 4
665
666 =item *
667
668 sep_char, quote_char, and escape_char are passed to Text::CSV on creation.
669 Changing them later has no effect currently.
670
671 =item *
672
673 Encoding errors are not dealt with properly.
674
675 =back
676
677 =head1 TODO
678
679 Dispatch to child objects, like this:
680
681  $csv = SL::Helper::Csv->new(
682    file    => ...
683    profile => [ {
684      profile => [
685        makemodel => {
686          make_1  => make,
687          model_1 => model,
688        },
689        makemodel => {
690          make_2  => make,
691          model_2 => model,
692        },
693      ],
694      class   => SL::DB::Part,
695    } ]
696  );
697
698 =head1 AUTHOR
699
700 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
701
702 =cut