SL::Helper::Csv: Doku zur Errorstruktur.
[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 Text::CSV;
9 use Params::Validate qw(:all);
10 use Rose::Object::MakeMethods::Generic scalar => [ qw(
11    file encoding sep_char quote_char escape_char header profile class
12    numberformat dateformat _io _csv _objects _parsed _data _errors
13 ) ];
14
15
16 # public interface
17
18 sub new {
19   my $class  = shift;
20   my %params = validate(@_, {
21     sep_char      => { default => ';' },
22     quote_char    => { default => '"' },
23     escape_char   => { default => '"' },
24     header        => { type    => ARRAYREF, optional => 1 },
25     profile       => { type    => HASHREF,  optional => 1 },
26     file          => 1,
27     encoding      => 0,
28     class         => 0,
29     numberformat  => 0,
30     dateformat    => 0,
31   });
32   my $self = bless {}, $class;
33
34   $self->$_($params{$_}) for keys %params;
35
36   $self->_io(IO::File->new);
37   $self->_csv(Text::CSV->new({
38     binary => 1,
39     sep_char    => $self->sep_char,
40     quote_char  => $self->quote_char,
41     escape_char => $self->escape_char,
42
43   }));
44   $self->_errors([]);
45
46   return $self;
47 }
48
49 sub parse {
50   my ($self, %params) = @_;
51
52   $self->_open_file;
53   return unless $self->_check_header;
54   return unless $self->_parse_data;
55
56   $self->_parsed(1);
57   return $self;
58 }
59
60 sub get_data {
61   $_[0]->_data;
62 }
63
64 sub get_objects {
65   my ($self, %params) = @_;
66   croak 'no class given'   unless $self->class;
67   croak 'must parse first' unless $self->_parsed;
68
69   $self->_make_objects unless $self->_objects;
70   return wantarray ? @{ $self->_objects } : $self->_objects;
71 }
72
73 sub errors {
74   @{ $_[0]->_errors }
75 }
76
77 sub check_header {
78   $_[0]->_check_header;
79 }
80
81 # private stuff
82
83 sub _open_file {
84   my ($self, %params) = @_;
85
86   $self->encoding($self->_guess_encoding) if !$self->encoding;
87
88   $self->_io->open($self->file, '<' . $self->_encode_layer)
89     or die "could not open file " . $self->file;
90
91   return $self->_io;
92 }
93
94 sub _check_header {
95   my ($self, %params) = @_;
96   return $self->header if $self->header;
97
98   my $header = $self->_csv->getline($self->_io);
99
100   $self->_push_error([
101     $self->_csv->error_input,
102     $self->_csv->error_diag,
103     0,
104   ]) unless $header;
105
106   $self->header($header);
107 }
108
109 sub _check_header_for_class {
110   my ($self, %params) = @_;
111   my @errors;
112
113   return unless $self->class;
114   return $self->header;
115
116   for my $method (@{ $self->header }) {
117     next if $self->class->can($self->_real_method($method));
118
119     push @errors, [
120       $method,
121       undef,
122       "header field $method is not recognized",
123       undef,
124       0,
125     ];
126   }
127
128   $self->_push_error(@errors);
129
130   return ! @errors;
131 }
132
133 sub _parse_data {
134   my ($self, %params) = @_;
135   my (@data, @errors);
136
137   $self->_csv->column_names(@{ $self->header });
138
139   while (1) {
140     my $row = $self->_csv->getline($self->_io);
141     last if $self->_csv->eof;
142
143     if ($row) {
144       my %hr;
145       @hr{@{ $self->header }} = @$row;
146       push @data, \%hr;
147     } else {
148       push @errors, [
149         $self->_csv->error_input,
150         $self->_csv->error_diag,
151         $self->_io->input_line_number,
152       ];
153     }
154   }
155
156   $self->_data(\@data);
157   $self->_push_error(@errors);
158
159   return ! @errors;
160 }
161
162 sub _encode_layer {
163   ':encoding(' . $_[0]->encoding . ')';
164 }
165
166 sub _make_objects {
167   my ($self, %params) = @_;
168   my @objs;
169
170   eval "require " . $self->class;
171   local $::myconfig{numberformat} = $self->numberformat if $self->numberformat;
172   local $::myconfig{dateformat}   = $self->dateformat   if $self->dateformat;
173
174   for my $line (@{ $self->_data }) {
175     push @objs, $self->class->new(
176       map {
177         $self->_real_method($_) => $line->{$_}
178       } grep { $_ } keys %$line
179     );
180   }
181
182   $self->_objects(\@objs);
183 }
184
185 sub _real_method {
186   my ($self, $arg) = @_;
187   ($self->profile && $self->profile->{$arg}) || $arg;
188 }
189
190 sub _guess_encoding {
191   # won't fix
192   'utf-8';
193 }
194
195 sub _push_error {
196   my ($self, @errors) = @_;
197   my @new_errors = ($self->errors, @errors);
198   $self->_errors(\@new_errors);
199 }
200
201
202 1;
203
204 __END__
205
206 =encoding utf-8
207
208 =head1 NAME
209
210 SL::Helper::Csv - take care of csv file uploads
211
212 =head1 SYNOPSIS
213
214   use SL::Helper::Csv;
215
216   my $csv = SL::Helper::Csv->new(
217     file        => \$::form->{upload_file},
218     encoding    => 'utf-8', # undef means utf8
219     sep_char    => ',',     # default ';'
220     quote_char  => ''',     # default '"'
221     header      => [qw(id text sellprice word)] # see later
222     profile    => { sellprice => 'sellprice_as_number' }
223     class       => 'SL::DB::CsvLine',   # if present, map lines to this
224   )
225
226   my $status  = $csv->parse;
227   my $hrefs   = $csv->get_data;
228   my @objects = $scv->get_objects;
229
230 =head1 DESCRIPTION
231
232 See Synopsis.
233
234 Text::CSV offeres already good functions to get lines out of a csv file, but in
235 most cases you will want those line to be parsed into hashes or even objects,
236 so this model just skips ahead and gives you objects.
237
238 Encoding autodetection is not easy, and should not be trusted. Try to avoid it
239 if possible.
240
241 =head1 METHODS
242
243 =over 4
244
245 =item C<new> PARAMS
246
247 Standard constructor. You can use this to set most of the data.
248
249 =item C<parse>
250
251 Do the actual work. Will return true ($self actually) if success, undef if not.
252
253 =item C<get_objects>
254
255 Parse the data into objects and return those.
256
257 This method will return list or arrayref depending on context.
258
259 =item C<get_data>
260
261 Returns an arrayref of the raw lines as hashrefs.
262
263 =item C<errors>
264
265 Return all errors that came up druing parsing. See error handling for detailed
266 information.
267
268 =back
269
270 =head1 PARAMS
271
272 =over 4
273
274 =item C<file>
275
276 The file which contents are to be read. Can be a name of a physical file or a
277 scalar ref for memory data.
278
279 =item C<encoding>
280
281 Encoding of the CSV file. Note that this module does not do any encoding
282 guessing.  Know what your data ist. Defaults to utf-8.
283
284 =item C<sep_char>
285
286 =item C<quote_char>
287
288 =item C<escape_char>
289
290 Same as in L<Text::CSV>
291
292 =item C<header> \@FIELDS
293
294 Can be an array of columns, in this case the first line is not used as a
295 header. Empty header fields will be ignored in objects.
296
297 =item C<profile> \%ACCESSORS
298
299 May be used to map header fields to custom accessors. Example:
300
301   { listprice => listprice_as_number }
302
303 In this case C<listprice_as_number> will be used to read in values from the
304 C<listprice> column.
305
306 =item C<class>
307
308 If present, the line will be handed to the new sub of this class,
309 and the return value used instead of the line itself.
310
311 =back
312
313 =head1 ERROR HANDLING
314
315 After parsing a file all errors will be accumulated into C<errors>.
316
317 Each entry is an arrayref with the following structure:
318
319  [
320  0  offending raw input,
321  1  Text::CSV error code if T:C error, 0 else,
322  2  error diagnostics,
323  3  position in line,
324  4  estimated line in file,
325  ]
326
327 Note that the last entry can be off, but will give an estimate.
328
329 =head1 CAVEATS
330
331 =over 4
332
333 =item *
334
335 sep_char, quote_char, and escape_char are passed to Text::CSV on creation.
336 Changing them later has no effect currently.
337
338 =item *
339
340 Encoding errors are not dealt with properly.
341
342 =back
343
344 =head1 TODO
345
346 Dispatch to child objects, like this:
347
348  $csv = SL::Helper::Csv->new(
349    file  => ...
350    class => SL::DB::Part,
351    profile => [
352      makemodel => {
353        make_1  => make,
354        model_1 => model,
355      },
356      makemodel => {
357        make_2  => make,
358        model_2 => model,
359      },
360    ]
361  );
362
363 =head1 AUTHOR
364
365 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
366
367 =cut