File Coverage

blib/lib/Fsdb/Filter/xml_to_db.pm
Criterion Covered Total %
statement 18 88 20.4
branch 0 20 0.0
condition n/a
subroutine 6 16 37.5
pod 5 5 100.0
total 29 129 22.4


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             #
4             # xml_to_db.pm
5             # Copyright (C) 2011-2015 by John Heidemann
6             # $Id: 54d87bcf09ffd487550f0f7e05ab0a77cf2a3670 $
7             #
8             # This program is distributed under terms of the GNU general
9             # public license, version 2. See the file COPYING
10             # in $dblib for details.
11             #
12              
13             package Fsdb::Filter::xml_to_db;
14              
15             =head1 NAME
16              
17             xml_to_db - convert a subset of XML into fsdb
18              
19             =head1 SYNOPSIS
20              
21             xml_to_db -k EntityField
22              
23             =head1 DESCRIPTION
24              
25             Converts a I subset of XML into Fsdb format.
26              
27             The input is XML-format (I fsdb).
28             The input is parsed as XML, and each entity
29             of type ENTITYFIELD is extracted as a row.
30             ENTITYFIELD can have mutliple components separated by slashes
31             to walk down the XML tree, if necessary.
32              
33             The input XML file is assumed to be I.
34             All rows are assumed to be sequential in one entity.
35             Any other than the specified ENTITYFIELD are ignored.
36             The schema is assumed to be defined by the first instances of that field.
37              
38             The output is two-space-separated fsdb.
39             (Someday more general field separators should be supported.)
40             Fsdb fields are normalized version of the CSV file:
41             spaces are converted to single underscores.
42              
43             =head1 OPTIONS
44              
45             =over 4
46              
47             =item B<-e> EmptyValue or B<--empty>
48              
49             Specify the value newly created columns get.
50              
51             =back
52              
53             =for comment
54             begin_standard_fsdb_options
55              
56             This module also supports the standard fsdb options:
57              
58             =over 4
59              
60             =item B<-d>
61              
62             Enable debugging output.
63              
64             =item B<-i> or B<--input> InputSource
65              
66             Read from InputSource, typically a file name, or C<-> for standard input,
67             or (if in Perl) a IO::Handle, Fsdb::IO or Fsdb::BoundedQueue objects.
68              
69             =item B<-o> or B<--output> OutputDestination
70              
71             Write to OutputDestination, typically a file name, or C<-> for standard output,
72             or (if in Perl) a IO::Handle, Fsdb::IO or Fsdb::BoundedQueue objects.
73              
74             =item B<--autorun> or B<--noautorun>
75              
76             By default, programs process automatically,
77             but Fsdb::Filter objects in Perl do not run until you invoke
78             the run() method.
79             The C<--(no)autorun> option controls that behavior within Perl.
80              
81             =item B<--help>
82              
83             Show help.
84              
85             =item B<--man>
86              
87             Show full manual.
88              
89             =back
90              
91             =for comment
92             end_standard_fsdb_options
93              
94              
95             =head1 SAMPLE USAGE
96              
97             =head2 Input:
98              
99            
100            
101            
102            
103            
104            
105            
106            
107            
108            
109            
110              
111             =head2 Command:
112              
113             xml_to_db -k files/file
114              
115             =head2 Output:
116              
117             #fsdb -F S addtime album artist title
118             3389919728 Born to Pick 7th Day Buskers Loch Lamor
119             3389919728 Born to Pick 7th Day Buskers The Floods
120             3389919735 Copland Conducts Copland Aaron Copland Our Town
121             # | xml_to_db -k files/file
122              
123             =head1 SEE ALSO
124              
125             L.
126              
127              
128             =head1 CLASS FUNCTIONS
129              
130             =cut
131              
132             @ISA = qw(Fsdb::Filter);
133             $VERSION = 2.0;
134              
135 1     1   6815 use strict;
  1         4  
  1         38  
136 1     1   8 use Pod::Usage;
  1         2  
  1         121  
137 1     1   10 use Carp;
  1         2  
  1         70  
138              
139 1     1   651 use XML::Simple;
  1         9587  
  1         12  
140              
141 1     1   101 use Fsdb::Filter;
  1         3  
  1         27  
142 1     1   6 use Fsdb::IO::Writer;
  1         4  
  1         1079  
143              
144              
145             =head2 new
146              
147             $filter = new Fsdb::Filter::xml_to_db(@arguments);
148              
149             Create a new xml_to_db object, taking command-line arguments.
150              
151             =cut
152              
153             sub new ($@) {
154 0     0 1   my $class = shift @_;
155 0           my $self = $class->SUPER::new(@_);
156 0           bless $self, $class;
157 0           $self->set_defaults;
158 0           $self->parse_options(@_);
159 0           $self->SUPER::post_new();
160 0           return $self;
161             }
162              
163              
164             =head2 set_defaults
165              
166             $filter->set_defaults();
167              
168             Internal: set up defaults.
169              
170             =cut
171              
172             sub set_defaults ($) {
173 0     0 1   my($self) = @_;
174 0           $self->{_entity} = undef;
175 0           $self->SUPER::set_defaults();
176             }
177              
178             =head2 parse_options
179              
180             $filter->parse_options(@ARGV);
181              
182             Internal: parse command-line arguments.
183              
184             =cut
185              
186             sub parse_options ($@) {
187 0     0 1   my $self = shift @_;
188              
189 0           my(@argv) = @_;
190             $self->get_options(
191             \@argv,
192 0     0     'help|?' => sub { pod2usage(1); },
193 0     0     'man' => sub { pod2usage(-verbose => 2); },
194             'autorun!' => \$self->{_autorun},
195             'd|debug+' => \$self->{_debug},
196             'e|empty=s' => \$self->{_empty},
197             'k|key|entity=s' => \$self->{_entity},
198 0     0     'i|input=s' => sub { $self->parse_io_option('input', @_); },
199             'log!' => \$self->{_logprog},
200 0     0     'o|output=s' => sub { $self->parse_io_option('output', @_); },
201 0 0         ) or pod2usage(2);
202 0 0         pod2usage(2) if ($#argv >= 0);
203             }
204              
205             =head2 _find_entities
206              
207             $filter->_find_entities
208              
209             Internal: walk the ENTITYFIELD specification through the XML::Simple data structure.
210             Returns an aref.
211              
212             =cut
213             sub _find_entities {
214 0     0     my($self) = @_;
215 0 0         croak $self->{_prog} . ": no XML input.\n" if (!defined($self->{_xml_in}));
216 0           my @entity_path = split(m@/@, $self->{_entity});
217 0           my $href = $self->{_xml_in};
218 0           my $last_entity = pop(@entity_path);
219 0           foreach (@entity_path) {
220             croak $self->{_prog} . ": missing element $_ of entity path.\n"
221 0 0         if (!defined($href->{$_}));
222             croak $self->{_prog} . ": element $_ of entity path is not href.\n"
223 0 0         if (ref($href->{$_}) ne 'HASH');
224 0           $href = $href->{$_};
225             };
226             croak $self->{_prog} . ": last $last_entity of entity path is not present.\n"
227 0 0         if (!defined($href->{$last_entity}));
228 0 0         if (ref($href->{$last_entity}) eq 'ARRAY') {
    0          
229 0           return $href->{$last_entity};
230             } elsif (ref($href->{$last_entity}) eq 'HASH') {
231 0           my @a;
232 0           $href = $href->{$last_entity};
233 0           foreach (keys %$href) {
234 0           push(@a, $href->{$_});
235             };
236 0           return \@a;
237             } else {
238 0           croak $self->{_prog} . ": last $last_entity of entity path is not an aref or href.\n"
239             };
240             }
241              
242             =head2 setup
243              
244             $filter->setup();
245              
246             Internal: setup, parse headers.
247              
248             =cut
249              
250             sub setup ($) {
251 0     0 1   my($self) = @_;
252              
253 0 0         pod2usage(2) if (!defined($self->{_entity}));
254              
255 0           $self->finish_fh_io_option('input');
256              
257 0           my $xs = $self->{_xs} = new XML::Simple;
258              
259             # xxx: have to read and parse the whole input, a no-no for setup() :-(
260 0           $self->{_xml_in} = $self->{_xs}->XMLin($self->{_in});
261 0           my $entities_aref = $self->_find_entities();
262              
263 0           my(@columns) = sort keys %{$entities_aref->[0]};
  0            
264              
265 0           $self->finish_io_option('output', -fscode => 'S', -cols => \@columns);
266             }
267              
268             =head2 run
269              
270             $filter->run();
271              
272             Internal: run over each rows.
273              
274             =cut
275             sub run ($) {
276 0     0 1   my($self) = @_;
277              
278 0           my $write_fastpath_sub = $self->{_out}->fastpath_sub();
279              
280 0           my $out = $self->{_out};
281 0           my $cols_aref = $out->cols();
282 0           my %cols_hash;
283 0           my $ncols = 0;
284 0           foreach (@$cols_aref) {
285 0           $cols_hash{$_} = $ncols++;
286             };
287              
288 0           my @empty_row = ($self->{_empty}) x $ncols;
289 0           my $record_no = 0;
290 0           foreach my $ent (@{$self->_find_entities()}) {
  0            
291 0           my(@row) = @empty_row;
292 0           $record_no++;
293 0           foreach (keys %$ent) {
294 0           my $i = $cols_hash{$_}; # $out->col_to_i($_);
295 0 0         if (!defined($i)) {
296 0           warn $self->{_prog} . ": unknown field $_ in record $record_no.\n";
297             } else {
298 0           $row[$i] = $ent->{$_};
299             };
300             };
301 0           grep { s/ +/ /g; } @row; # clean up for fsdb double-space separator
  0            
302 0           grep { s/^ *$/$self->{_empty}/g; } @row; # add null values for fields
  0            
303 0           &{$write_fastpath_sub}(\@row);
  0            
304             };
305             }
306              
307              
308             =head1 AUTHOR and COPYRIGHT
309              
310             Copyright (C) 2011-2015 by John Heidemann
311              
312             This program is distributed under terms of the GNU general
313             public license, version 2. See the file COPYING
314             with the distribution for details.
315              
316             =cut
317              
318             1;