File Coverage

blib/lib/PICA/Writer.pm
Criterion Covered Total %
statement 121 229 52.8
branch 43 134 32.0
condition 11 47 23.4
subroutine 26 28 92.8
pod 13 13 100.0
total 214 451 47.4


line stmt bran cond sub pod time code
1             package PICA::Writer;
2             {
3             $PICA::Writer::VERSION = '0.585';
4             }
5             #ABSTRACT: Write and count PICA+ records and fields
6 13     13   1080 use strict;
  13         24  
  13         688  
7              
8              
9 13     13   917 use PICA::Record;
  13         26  
  13         597  
10 13     13   73 use XML::Writer;
  13         20  
  13         241  
11 13     13   4002 use PICA::Parser;
  13         41  
  13         843  
12 13     13   76 use IO::Handle;
  13         32  
  13         445  
13 13     13   13684 use IO::Scalar;
  13         65593  
  13         661  
14 13     13   11506 use IO::File;
  13         30856  
  13         2129  
15 13     13   12171 use String::Escape qw(qqbackslash elide);
  13         90688  
  13         1596  
16 13     13   124 use Carp qw(croak);
  13         37  
  13         651  
17              
18 13     13   75 use constant ERROR => 0;
  13         26  
  13         1154  
19 13     13   79 use constant NEW => 1;
  13         27  
  13         568  
20 13     13   128 use constant STARTED => 2;
  13         28  
  13         536  
21 13     13   64 use constant ENDED => 3;
  13         26  
  13         1068  
22              
23             use overload
24 13     13   435 'bool' => sub { $_[0]->status };
  13     6   29  
  13         136  
  6         29  
25              
26              
27             sub new {
28 6     6 1 3234 my $class = shift;
29 6         55 my $self = bless {
30             status => NEW,
31             io => undef,
32             options => {},
33             recordcounter => 0,
34             fieldcounter => 0,
35             }, $class;
36 6 100       42 return $self->reset( @_ ? @_ : undef );
37             }
38              
39              
40             sub output {
41 6     6 1 13 my $self = shift;
42 6 50       70 my ($output, %options) = @_ % 2 ? @_ : (undef, @_);
43              
44 6         15 %{ $self->{options} } = %options;
  6         153  
45 6         18 my $format = $self->{options}->{format};
46              
47 6 100       62 if (not defined $output) {
    50          
    50          
    100          
48 2         5 $self->{io} = undef;
49             } elsif ( ref($output) eq 'GLOB' ) {
50 0         0 $self->{io} = $output;
51 0         0 PICA::Parser::enable_binmode_encoding( $self->{io} );
52             } elsif ( UNIVERSAL::isa('IO::Handle', $output) ) {
53 0         0 $self->{io} = $output;
54 0         0 PICA::Parser::enable_binmode_encoding( $self->{io} );
55             } elsif ( ref($output) eq 'SCALAR' ) {
56 1         10 $self->{io} = IO::Scalar->new( $output );
57             } else {
58 3         24 $self->{io} = IO::File->new($output, '>:utf8');
59 3 50 33     517 $format = 'xml' if not defined $format and $output =~ /\.xml$/;
60             }
61              
62 6 50       323 if ($options{pretty}) {
63 0         0 $options{DATA_MODE} = 1;
64 0         0 $options{DATA_INDENT} = 2;
65 0         0 $options{NAMESPACES} = 1;
66 0         0 $options{PREFIX_MAP} = {'info:srw/schema/5/picaXML-v1.0'=>''};
67             }
68              
69 6 50 33     39 $format = 'plain' unless defined $format and $format =~ /^(plain|normalized|xml)$/i;
70              
71 6         26 $self->{options}->{format} = lc($format);
72 6 50 33     38 if ( $format =~ /^xml$/i and defined $output ) {
73 0         0 $options{OUTPUT} = $self->{io};
74 0 0       0 $options{header} = 1 unless defined $options{header};
75 0         0 $self->{xmlwriter} = PICA::Writer::xmlwriter( %options );
76             } else {
77 6         17 $self->{xmlwriter} = undef;
78             }
79            
80 6 50 66     63 if (defined $output and not $self->{io}) {
81 0         0 $self->{status} = ERROR;
82             }
83              
84 6 50       27 if ( $self->{options}->{stats} ) {
85 0         0 $self->{fieldstat} = {};
86 0 0       0 $self->{subfieldstat} = {} if $self->{options}->{stats} > 1;
87             } else {
88 6         14 $self->{subfieldstat} = undef;
89 6         23 $self->{fieldstat} = undef;
90             }
91              
92 6         16 return $self;
93             }
94              
95              
96             sub reset {
97 7     7 1 15 my $self = shift;
98 7 100       45 $self->output( @_ ) if @_;
99              
100 7         18 $self->{recordcounter} = 0;
101 7         14 $self->{fieldcounter} = 0;
102            
103 7         30 return $self;
104             }
105              
106              
107             sub write {
108 9     9 1 46 my $self = shift;
109 9 50       47 croak('cannot write to a closed writer') if $self->status == ENDED;
110 9 100       41 $self->start if $self->status != STARTED;
111              
112 9         27 my $format = $self->{options}->{format};
113              
114 9 50       78 if (UNIVERSAL::isa($_[0],'PICA::Field')) {
115 0         0 while (@_) {
116 0         0 my $field = shift;
117 0 0       0 if (UNIVERSAL::isa($field,'PICA::Field')) {
118 0 0 0     0 if ($format eq 'plain') {
    0          
    0          
119 0 0       0 print { $self->{io} } $field->string if $self->{io};
  0         0  
120             } elsif ($format eq 'normalized') {
121 0 0       0 print { $self->{io} } $field->normalized() if $self->{io};
  0         0  
122             } elsif ($format eq 'xml' and defined $self->{xmlwriter} ) {
123 0         0 $field->xml( $self->{xmlwriter} );
124             }
125 0         0 $self->addfieldstat( $field );
126             } else {
127 0         0 croak("Cannot write object of unknown type (PICA::Field expected)!");
128             }
129             }
130             } else {
131 9         19 my $comment = "";
132 9         47 while (@_) {
133 10         17 my $record = shift;
134 10 50       40 if ( UNIVERSAL::isa($record, 'PICA::Record') ) {
    0          
135 10 50 0     29 if ($format eq 'plain') {
    0          
    0          
136 10 100 100     66 print { $self->{io} } "\n"
  1         12  
137             if ($self->{recordcounter} > 0 && $self->{io});
138 10 100       47 print { $self->{io} } $record->string if $self->{io};
  5         70  
139             } elsif ($format eq 'normalized') {
140 0 0 0     0 print { $self->{io} } "\x1D\x0A" # next record
  0         0  
141             if ($self->{recordcounter} > 0 && $self->{io});
142 0 0       0 print { $self->{io} } $record->normalized() if $self->{io};
  0         0  
143             } elsif ($format eq 'xml' and defined $self->{xmlwriter} ) {
144 0         0 $record->xml( $self->{xmlwriter} );
145             }
146 10         70 $self->addrecordstat( $record );
147             } elsif (ref(\$record) eq 'SCALAR') {
148 0 0       0 next if !$record;
149 0         0 $comment = '# ' . join("\n# ", split(/\n/,$record)) . "\n";
150 0         0 $comment =~ s/--//g;
151 0 0       0 if ($format eq 'xml') {
152 0 0       0 $self->{xmlwriter}->comment( $comment )
153             if defined $self->{xmlwriter};
154             } else {
155 0 0       0 print { $self->{io} } $comment if $self->{io};
  0         0  
156             }
157             } else {
158 0         0 croak("Cannot write object of unknown type (PICA::Record expected)!");
159             }
160             }
161             }
162              
163 9         29 return $self;
164             }
165              
166              
167             sub start {
168 6     6 1 11 my $self = shift;
169 6 50       17 croak('cannot start a writer twice') if $self->status == STARTED;
170 6 50       17 croak('cannot start a writer in error status') if $self->status == ERROR;
171              
172 6         14 my $writer = $self->{xmlwriter};
173 6 50 33     36 if ( $self->{options}->{format} eq 'xml' and defined $writer ) {
174 0 0       0 if (UNIVERSAL::isa( $writer, 'XML::Writer::Namespaces' )) {
175 0         0 $writer->startTag( [$PICA::Record::XMLNAMESPACE, 'collection'] );
176             } else {
177 0         0 $writer->startTag( 'collection' );
178             }
179             }
180              
181 6         14 $self->{status} = STARTED;
182              
183 6         21 return $self;
184             }
185              
186              
187              
188             sub end {
189 4     4 1 9 my $self = shift;
190 4 50       15 croak('cannot end a writer in error status') if $self->status == ERROR;
191 4 50       14 croak('cannot end a writer twice') if $self->status == ENDED;
192 4 50       11 $self->start if $self->status != STARTED;
193              
194 4 50       17 if ( $self->{options}->{format} eq 'xml') {
195 0 0       0 if ( defined $self->{xmlwriter} ) {
196 0         0 $self->{xmlwriter}->endTag(); #
197 0         0 $self->{xmlwriter}->end();
198             }
199             } else {
200             # other supported formats don't need end handling
201             }
202              
203 4 50       38 $self->{io}->close if defined $self->{io};
204 4         313 $self->{status} = ENDED;
205            
206 4         17 return $self;
207             }
208              
209              
210             sub status {
211 48     48 1 59 my $self = shift;
212 48         188 return $self->{status};
213             }
214              
215              
216             sub records {
217 5     5 1 46 my $self = shift;
218 5         42 return $self->{recordcounter};
219             }
220              
221              
222             *counter = *records;
223              
224              
225             sub fields {
226 3     3 1 8 my $self = shift;
227 3         20 return $self->{fieldcounter};
228             }
229              
230              
231             sub statlines {
232 0     0 1 0 my $self = shift;
233              
234 0         0 my @STRINGS = ('?',' ','*','+');
235 0         0 my @stats = ();
236              
237 0   0     0 my $fieldstat = $self->{fieldstat} || { };
238 0         0 my $subfieldstat = $self->{subfieldstat};
239              
240 0         0 foreach my $tag (sort { $a cmp $b } keys %{$fieldstat}) {
  0         0  
  0         0  
241 0 0       0 my $line = length($tag) < 5 ? "$tag " : "$tag ";
242 0         0 $line .= $STRINGS[ $fieldstat->{$tag} ];
243 0 0       0 if ( defined $subfieldstat ) {
244 0         0 my $s = $subfieldstat->{$tag};
245 0         0 foreach (keys %{$s}) {
  0         0  
246 0         0 $line .= "\$$_ ";
247 0         0 $line .= $STRINGS[ $s->{$_}->{occ} ];
248 0 0       0 $line .= qqbackslash(elide($s->{$_}->{val},40))
249             if defined $s->{$_}->{val};
250 0         0 $line .= " "; # TODO: join!
251             }
252             }
253 0         0 push @stats, $line;
254             }
255              
256 0         0 return @stats;
257             }
258              
259              
260              
261             sub xmlwriter {
262 6     6 1 13 my %params = @_;
263              
264 6 100       21 $params{NAMESPACES} = 1 unless defined $params{NAMESPACES};
265 6 100 66     24 if (not defined $params{PREFIX_MAP} or
266             not defined $params{PREFIX_MAP}->{ $PICA::Record::XMLNAMESPACE }) {
267 4         16 $params{PREFIX_MAP} = { $PICA::Record::XMLNAMESPACE => 'pica' };
268             }
269 6         34 my $writer = XML::Writer->new( %params );
270 6 100       1358 $writer->xmlDecl('UTF-8') if $params{header};
271 6 50       37 if ($params{xslt}) {
272 0         0 $writer->pi('xml-stylesheet', 'type="text/xsl" href="' . $params{xslt} . '"');
273             }
274              
275 6         20 return $writer;
276             }
277              
278              
279             sub addfieldstat {
280 0     0 1 0 my ($self, $field) = @_;
281 0         0 $self->{fieldcounter}++;
282              
283 0 0       0 return unless defined $self->{subfieldstat};
284              
285 0         0 my $tag = $field->tag;
286 0         0 my (%o,%v);
287              
288 0         0 my @content = $field->content;
289             #print Dumper($field->content);
290 0         0 foreach (@content) {
291 0         0 my ($sf,$value) = @{$_};
  0         0  
292              
293 0         0 $o{ $sf }++;
294 0 0       0 if ( exists $v{ $sf } ) {
295 0 0 0     0 $v{ $sf } = undef unless defined $v{ $sf } and $v{ $sf } eq $value;
296             } else {
297 0         0 $v{ $sf } = $value;
298             }
299             }
300              
301 0         0 my $sfstat = $self->{subfieldstat};
302              
303             # TODO: order of subfields
304 0         0 my $all = $sfstat->{$tag};
305 0 0       0 if ( $sfstat->{$tag} ) {
306 0         0 foreach my $sf (keys %{$sfstat->{$tag}}) {
  0         0  
307 0         0 my $cur = $sfstat->{$tag}->{$sf};
308 0 0       0 if ( $o{$sf} ) { # this time also
309             # ..
310 0 0 0     0 $cur->{occ} += 2
311             if $o{$sf} > 1 and $cur->{occ} < 2;
312              
313 0 0 0     0 $cur->{val} = undef unless
      0        
314             defined $v{$sf} and defined $cur->{val} and $v{$sf} eq $cur->{val};
315 0         0 delete $v{$sf};
316 0         0 delete $o{$sf};
317             } else { # not this time but before
318 0 0       0 $cur->{occ} = $cur->{occ} > 1 ? 0 : 2;
319             }
320             }
321              
322             # fehlende subfields hinzufügen
323 0         0 foreach (keys %o) {
324 0         0 $sfstat->{$tag}->{$_} = { val => $v{$_}, occ => $o{$_} };
325             }
326             } else {
327 0         0 $sfstat->{$tag} = {
328 0         0 map { $_ => { val => $v{$_}, occ => $o{$_} } } keys %o
329             };
330             }
331            
332             # ...stats...
333             }
334              
335              
336             sub addrecordstat {
337 10     10 1 22 my ($self, $record) = @_;
338 10         22 $self->{recordcounter}++;
339              
340 10 50       42 if ( not defined $self->{fieldstat} ) {
341 10         48 $self->{fieldcounter} += scalar $record->fields;
342 10         48 return;
343             }
344 0           my $fieldstat = $self->{fieldstat};
345            
346             # add field stats
347 0           my %count; # undef, one, repeatable
348 0           foreach my $field ($record->fields) {
349 0           $self->addfieldstat( $field );
350 0           $count{ $field->tag }++;
351             }
352              
353             # 1, 3 : unique
354             # 0,1,2,3 ?: optional, 1: mandatory, +: repeatable
355              
356 0           foreach my $tag (keys %{$fieldstat}) {
  0            
357 0 0         if ( $count{$tag} ) { # does exist this time but before only once
358 0 0 0       if ( $count{$tag} > 1 and $fieldstat->{$tag} < 2 ) {
359 0           $fieldstat->{$tag} += 2;
360             } #if $fieldstat->{$tag} < 2;
361 0           delete $count{$tag};
362             } else { # has existed before but not this time
363 0 0         $fieldstat->{$tag} = $fieldstat->{$tag} > 1 ? 0 : 2;
364             }
365             }
366              
367             # new fields are '1' or '+'
368 0           foreach my $tag (keys %count) {
369 0 0         $fieldstat->{$tag} = $count{$tag} > 1 ? 3 : 1; #
370             }
371             }
372              
373             1;
374              
375             __END__