File Coverage

blib/lib/Text/CSV/Track.pm
Criterion Covered Total %
statement 251 253 99.2
branch 108 124 87.1
condition 3 3 100.0
subroutine 26 26 100.0
pod 11 11 100.0
total 399 417 95.6


line stmt bran cond sub pod time code
1             package Text::CSV::Track;
2              
3             our $VERSION = '1.00';
4 3     3   202272 use 5.006;
  3         9  
5              
6 3     3   16 use strict;
  3         7  
  3         82  
7 3     3   11 use warnings;
  3         12  
  3         210  
8              
9 3     3   15 use base qw(Class::Accessor::Fast);
  3         10  
  3         1512  
10             __PACKAGE__->mk_accessors(
11             qw(
12             file_name
13             _file_fh
14             _rh_value_of
15             _lazy_init
16             ignore_missing_file
17             full_time_lock
18             auto_store
19             _no_lock
20             ignore_badly_formated
21             _csv_format
22             header_lines
23             footer_lines
24             hash_names
25             single_column
26             trunc
27             replace_new_lines_with
28             identificator_column_number
29              
30             sep_char
31             escape_char
32             quote_char
33             always_quote
34             binary
35             type
36             )
37             );
38              
39 3     3   13209 use FindBin;
  3         4114  
  3         164  
40              
41 3     3   3356 use Text::CSV_XS;
  3         69854  
  3         198  
42 3     3   1955 use Carp::Clan;
  3         11975  
  3         53  
43 3     3   2347 use English qw(-no_match_vars);
  3         13643  
  3         25  
44 3     3   1367 use Fcntl ':flock'; # import LOCK_* constants
  3         6  
  3         381  
45 3     3   19 use Fcntl ':seek'; # import SEEK_* constants
  3         4  
  3         246  
46 3     3   2437 use List::MoreUtils qw { first_index };
  3         53926  
  3         37  
47 3     3   3752 use IO::Handle; #must be because file_fh->input_line_number function
  3         7  
  3         7902  
48              
49              
50             sub new {
51 62     62 1 715493 my $class = shift;
52 62         123 my $ra_arg = shift;
53              
54             #build object from parent
55 62         380 my $self = $class->SUPER::new($ra_arg);
56              
57             #create empty pointers
58 62         754 $self->{_rh_value_of} = {};
59 62 100       361 $self->{header_lines} = [] if not defined $self->{header_lines};
60 62 100       283 $self->{footer_lines} = [] if not defined $self->{footer_lines};
61              
62 62         238 return $self;
63             }
64              
65             sub output_row_of {
66 1557     1557 1 2196 my $self = shift;
67 1557         2494 my $identificator = shift;
68 1557         2156 my $type = shift;
69              
70             #combine values for csv file
71 1557         3317 my @fields = $self->value_of($identificator);
72              
73             #removed entry
74 1557 100 100     5967 return undef if (@fields == 1) and (not defined $fields[0]);
75              
76             #if in single column mode remove '1' from the start of the fields
77 1553 100       31516 shift(@fields) if $self->single_column;
78              
79             #remove new lines
80 1553 50       33634 if (defined $self->replace_new_lines_with) {
81 1553         33474 my $replacement = $self->replace_new_lines_with;
82 1553         6693 foreach my $field (@fields) {
83 1558 100       15153 next if not defined $field;
84 1556         3943 $field =~ s{[\n\r]+}{$replacement}sg;
85             }
86             }
87              
88             #add identificator to the values
89 1553         29478 splice(@fields, $self->identificator_column_number, 0, $identificator);
90              
91 1553 100       8210 if ($type eq 'csv') {
    50          
92 1549 100       29297 croak "invalid value to store to an csv file - ", $self->_csv_format->error_input(),"\n"
93             if (not $self->_csv_format->combine(@fields));
94              
95 1548         60737 return $self->_csv_format->string();
96             }
97             elsif ($type eq 'xml') {
98 4         8 my $xml_line = ''."\n";
99 4         12 $xml_line .= ' '.$identificator.''."\n";
100 4         55 foreach my $col_value ($self->value_of($identificator)) {
101 4 50       13 $col_value = '' if not defined $col_value;
102 4         12 $xml_line .= ' '.$col_value.''."\n";
103             }
104 4         11 $xml_line .= '';
105              
106 4         14 return $xml_line;
107             }
108             else {
109 0         0 croak "unknow output format";
110             }
111             }
112              
113              
114             sub csv_line_of {
115 1     1 1 861 my $self = shift;
116 1         5 my $identificator = shift;
117              
118 1         5 return $self->output_row_of($identificator, 'csv');
119             }
120              
121              
122             sub value_of {
123 6948     6948 1 14279 my $self = shift;
124 6948         9873 my $identificator = shift;
125 6948         9263 my $is_set = 0; #by default get
126              
127             #if we have one more parameter then it is set
128 6948         9265 my $value;
129 6948 100       14880 if (@_ >= 1) {
130 2733         3649 $is_set = 1;
131 2733         4511 $value = \@_;
132             }
133              
134             #check if we have identificator
135 6948 50       13006 return if not $identificator;
136              
137             #value_of hash
138 6948         143776 my $rh_value_of = $self->_rh_value_of;
139              
140             #lazy initialization is needed for get
141 6948 100       39255 $self->_init() if not $is_set;
142              
143             #switch between set and get variant
144             #set
145 6947 100       27676 if ($is_set) {
146 2733         9316 $rh_value_of->{$identificator} = $value;
147             }
148             #get
149             else {
150 4214 100       12152 return undef if not defined $rh_value_of->{$identificator};
151              
152             #if we have more then one field return array
153 1642 100       1981 if (@{$rh_value_of->{$identificator}} > 1) {
  1642         3363  
154 21         37 return @{$rh_value_of->{$identificator}};
  21         159  
155             }
156             #otherwise return one and only value from array as scallar
157             else {
158 1621         2093 return ${$rh_value_of->{$identificator}}[0];
  1621         5701  
159             }
160             }
161             }
162              
163              
164             sub hash_of {
165 8     8 1 7046 my $self = shift;
166 8         22 my $identificator = shift;
167 8         30 my $is_set = 0; #by default get
168              
169 8 50       347 croak "'hash_names' parameter not set" if not defined $self->hash_names;
170 8         70 my @hash_names = @{$self->hash_names};
  8         209  
171 8         73 my @fields = $self->value_of($identificator);
172              
173             #if we have one more parameter then it is set
174 8         20 my $rh;
175 8 100       27 if (@_ >= 1) {
176 3         6 $is_set = 1;
177 3         7 $rh = shift;
178              
179 3 50       22 croak "not a hash reference as set argument" if ref $rh ne 'HASH';
180             }
181              
182 8 100       23 if ($is_set) {
183 3         6 foreach my $key (keys %{$rh}) {
  3         14  
184 6     23   74 my $index = first_index { $_ eq $key } @hash_names;
  23         69  
185              
186 6 100       37 croak "no such hash key name '$key'" if $index == -1;
187              
188 5         17 $fields[$index] = $rh->{$key};
189             }
190              
191             #save back the fields
192 2         13 $self->value_of($identificator, @fields);
193             }
194             else {
195 5         12 my %hash;
196 5         12 foreach my $name (@hash_names) {
197 22         65 $hash{$name} = shift @fields;
198             }
199              
200 5         62 return \%hash;
201             }
202             }
203              
204              
205             sub store_as_xml {
206 1     1 1 6 my $self = shift;
207              
208 1         5 return $self->store(1);
209             }
210              
211              
212             sub store {
213 36     36 1 3457 my $self = shift;
214 36         101 my $store_as_xml = shift;
215              
216             #lazy initialization
217 36         122 $self->_init();
218              
219             #get local variables from self hash
220 36         1066 my $rh_value_of = $self->_rh_value_of;
221 36         1007 my $file_name = $self->file_name;
222 36         1027 my $full_time_lock = $self->full_time_lock;
223 36         884 my $file_fh = $self->_file_fh;
224              
225 36 50       220 if (not $full_time_lock) {
226 36 50       2599 open($file_fh, "+>>", $file_name) or croak "can't write to file '$file_name' - $OS_ERROR";
227              
228             #lock and truncate the access store file
229 36 50       452 flock($file_fh, LOCK_EX) or croak "can't lock file '$file_name' - $OS_ERROR\n";
230             }
231              
232             #loop through identificators and store to array only if all works fine file will be overwritten
233 36         73 my @file_lines;
234 36         133 foreach my $identificator (sort $self->ident_list()) {
235 1556         2356 my $file_line;
236 1556 100       2917 if (defined $store_as_xml) {
237 4         12 $file_line = $self->output_row_of($identificator, 'xml');
238             }
239             else {
240 1552         3402 $file_line = $self->output_row_of($identificator, 'csv');
241             }
242              
243             #skip removed entries
244 1555 100       17179 next if not $file_line;
245              
246 1551         4190 push(@file_lines, $file_line."\n");
247             }
248              
249             #truncate the file so that we can store new results
250 35 50       3651 truncate($file_fh, 0) or croak "can't truncate file '$file_name' - $OS_ERROR\n";
251              
252             #write header lines
253 35         123 foreach my $header_line (@{$self->header_lines}) {
  35         161  
254 31         80 print {$file_fh} $header_line, "\n";
  31         154  
255             }
256              
257             #write csv lines
258 35         108 foreach my $line (@file_lines) {
259             #print the line to csv file
260 1551         1948 print {$file_fh} $line;
  1551         3767  
261             }
262              
263             #write footer lines
264 35         64 foreach my $footer_line (@{$self->footer_lines}) {
  35         147  
265 12         20 print {$file_fh} $footer_line, "\n";
  12         25  
266             }
267              
268 35         16256 close($file_fh);
269             }
270              
271             #lazy initialization
272             sub _init {
273 4361     4361   6915 my $self = shift;
274              
275 4361 100       86708 return if $self->_lazy_init;
276              
277             #prevent from reexecuting
278 62         1539 $self->_lazy_init(1);
279              
280             #default values
281 62 100       1850 $self->replace_new_lines_with('|') if not exists $self->{'replace_new_lines_with'};
282 62 100       1730 $self->binary(1) if not exists $self->{'binary'};
283 62 100       1691 $self->identificator_column_number(0) if not exists $self->{'identificator_column_number'};
284              
285             #get local variables from self hash
286 62         1516 my $rh_value_of = $self->_rh_value_of;
287 62         1486 my $file_name = $self->file_name;
288 62         1547 my $ignore_missing_file = $self->ignore_missing_file;
289 62         1557 my $full_time_lock = $self->full_time_lock;
290 62         2362 my $_no_lock = $self->_no_lock;
291 62         432 my $header_lines_count;
292             my $header_lines_from_file;
293 62         0 my $footer_lines_count;
294 62         0 my $footer_lines_from_file;
295              
296 62 100       225 if (ref $self->{header_lines} eq 'ARRAY') {
297 50         73 $header_lines_count = scalar @{$self->header_lines};
  50         169  
298 50         111 $header_lines_from_file = 0;
299             }
300             else {
301             #initialize header_lines with array of empty strings if header_lines is number
302 12         27 $header_lines_count = $self->{header_lines};
303 12         38 $self->header_lines([ map {""} (1 .. $header_lines_count) ]);
  34         105  
304 12         27 $header_lines_from_file = 1;
305             }
306              
307 62 100       203 if (ref $self->{footer_lines} eq 'ARRAY') {
308 56         84 $footer_lines_count = scalar @{$self->footer_lines};
  56         164  
309 56         96 $footer_lines_from_file = 0;
310             }
311             else {
312             #initialize footer_lines with array of empty strings if footer_lines is number
313 6         15 $footer_lines_count = $self->{footer_lines};
314 6         43 $self->footer_lines([ map {""} (1 .. $footer_lines_count) ]);
  18         90  
315 6         13 $footer_lines_from_file = 1;
316             }
317              
318             #Text::CSV_XS variables
319 62 100       1446 my $sep_char = defined $self->sep_char ? $self->sep_char : q{,};
320 62 100       1556 my $escape_char = defined $self->escape_char ? $self->escape_char : q{\\};
321 62 100       1841 my $quote_char = defined $self->quote_char ? $self->quote_char : q{"};
322 62         1558 my $always_quote = $self->always_quote;
323 62         1354 my $binary = $self->binary;
324              
325             #done with initialization if file_name empty
326 62 100       370 return if not $file_name;
327              
328             #define csv format
329 59         864 $self->_csv_format(Text::CSV_XS->new({
330             sep_char => $sep_char,
331             escape_char => $escape_char,
332             quote_char => $quote_char,
333             always_quote => $always_quote,
334             binary => $binary,
335             }));
336              
337             #default open mode is reading
338 59         15644 my $open_mode = '<';
339              
340             #if full_time_lock is set do open for writting
341 59 100       200 if ($full_time_lock) {
342 3 50       15 if ($ignore_missing_file) {
343 0         0 $open_mode = '+>>';
344             }
345             else {
346 3         7 $open_mode = '+<';
347             }
348             }
349              
350             #open file with old stored values and handle error
351 59         99 my $file_fh;
352 59 100       3367 if (not open($file_fh, $open_mode, $file_name)) {
353 7 100       31 if ($ignore_missing_file) {
354 6         22 $OS_ERROR = undef;
355 6         43 return;
356             }
357             else {
358 1         14 croak "can't read file '$file_name' - $OS_ERROR";
359             }
360             }
361              
362             #do exclusive lock if full time lock
363 52 100       235 if ($full_time_lock) {
    50          
364 3 50       33 flock($file_fh, LOCK_EX) or croak "can't lock file '$file_name' - $OS_ERROR\n";
365 3         16 seek($file_fh, 0, SEEK_SET);
366             }
367             #internal flag. used from within the same module if file is already locked
368             elsif ($_no_lock) {
369             }
370             #otherwise shared lock is enought
371             else {
372 49 50       457 flock($file_fh, LOCK_SH) or croak "can't lock file '$file_name' - $OS_ERROR\n";
373             }
374              
375 52         100 my $lines_count = 0;
376 52         3341 $lines_count++ while (<$file_fh>);
377              
378             #reset file position
379 52         282 seek($file_fh, 0, SEEK_SET);
380 52         380 $file_fh->input_line_number(0);
381              
382             #create hash of identificator => 1
383 52         2661 my %identificator_exist = map { $_ => 1 } $self->ident_list;
  24         85  
384              
385             #parse lines and store values in the hash
386             LINE:
387 52         683 while (my $line = <$file_fh>) {
388 2625         4397 chomp($line);
389 2625         3573 $lines_count--;
390              
391             #skip header lines and save them for store()
392 2625 100       4897 if ($header_lines_count) {
393             #save header line if not defined
394 33 100       81 ${$self->header_lines}[$file_fh->input_line_number-1] = $line if $header_lines_from_file;
  30         68  
395              
396             #decrease header lines code so then we will know when there is an end of headers
397 33         965 $header_lines_count--;
398              
399 33         127 next;
400             }
401              
402             #skip footer lines and save them for store()
403 2592 100       4848 if ($lines_count < $footer_lines_count) {
404             #save footer lines if not defined
405 15 50       36 ${$self->footer_lines}[$footer_lines_count - $lines_count - 1] = $line if $footer_lines_from_file;
  15         35  
406              
407 15         171 next;
408             }
409              
410             #skip reading of values if in 'trunc' mode
411 2577 100       55074 next if $self->trunc;
412              
413             #verify line. if incorrect skip with warning
414 2567 100       57216 if (!$self->_csv_format->parse($line)) {
415 3         163 my $msg = "badly formated '$file_name' csv line " . $file_fh->input_line_number() . " - '$line'.\n";
416              
417             #by default croak on bad line
418 3 100       234 croak $msg if not $self->ignore_badly_formated;
419              
420             #if ignore_badly_formated_lines is on just print warning
421 2         82 warn $msg;
422              
423 2         18 next;
424             }
425              
426             #extract fields
427 2564         122987 my @fields = $self->_csv_format->fields();
428 2564         72160 my $identificator = splice(@fields, $self->identificator_column_number, 1);
429              
430             #if in single column mode insert '1' to the fields
431 2564 100       56733 unshift(@fields, 1) if $self->single_column;
432              
433             #save present fields
434 2564         14119 my @old_fields = $self->value_of($identificator);
435              
436             #set the value from file
437 2564         6545 $self->value_of($identificator, @fields);
438              
439             #set the value from before values from file was read !needed becouse of the strategy!
440 2564 100       13017 $self->value_of($identificator, @old_fields) if $identificator_exist{$identificator};
441             }
442              
443             #if full time lock then store file handle
444 51 100       172 if ($full_time_lock) {
445 3         77 $self->_file_fh($file_fh);
446             }
447             #otherwise release shared lock and close file
448             else {
449 48 50       450 flock($file_fh, LOCK_UN) if not $_no_lock;
450 48         1156 close($file_fh);
451             }
452             }
453              
454              
455             sub ident_list {
456 105     105 1 276 my $self = shift;
457              
458             #lazy initialization
459 105         380 $self->_init();
460              
461             #get local variables from self hash
462 104         2739 my $rh_value_of = $self->_rh_value_of;
463              
464 104         485 return keys %{$rh_value_of};
  104         1367  
465             }
466              
467              
468             sub header_lines {
469 130     130 1 359 my $self = shift;
470              
471             #set
472 130 100       349 if (@_ >= 1) {
473 12         30 $self->{header_lines} = shift;
474             } else
475             #get
476             {
477             #if _header_lines then do lazy init and get the header lines from file
478 118 100       431 $self->_init if (ref $self->{header_lines} ne 'ARRAY');
479              
480 118         470 return $self->{header_lines};
481             }
482              
483             }
484              
485              
486             sub footer_lines {
487 119     119 1 205 my $self = shift;
488              
489             #set
490 119 100       278 if (@_ >= 1) {
491 9         24 $self->{footer_lines} = shift;
492             } else
493             #get
494             {
495             #if footer_lines is not array then do lazy init and get the footer lines from file
496 110 100       341 $self->_init if (ref $self->{footer_lines} ne 'ARRAY');
497              
498 110         344 return $self->{footer_lines};
499             }
500             }
501              
502              
503             sub finish {
504 62     62 1 102 my $self = shift;
505              
506             #call store if in auto_store mode
507 62 100       2397 $self->store() if $self->auto_store;
508              
509             #get local variables from self hash
510 62         1959 my $file_fh = $self->_file_fh;
511              
512 62 100       409 if (defined $file_fh) {
513 3         68 close($file_fh);
514             }
515              
516 62         1284 $self->_file_fh(undef);
517             }
518              
519             sub DESTROY {
520 62     62   10910 my $self = shift;
521              
522 62         275 $self->finish();
523             }
524              
525             1;
526              
527             __END__