File Coverage

blib/lib/XML/Directory.pm
Criterion Covered Total %
statement 110 325 33.8
branch 38 170 22.3
condition 1 29 3.4
subroutine 17 34 50.0
pod 0 19 0.0
total 166 577 28.7


line stmt bran cond sub pod time code
1             package XML::Directory;
2              
3             require 5.005_03;
4 1 50   1   768 BEGIN { require warnings if $] >= 5.006; }
5              
6 1     1   7 use strict;
  1         1  
  1         43  
7 1     1   6 use vars qw(@ISA @EXPORT_OK $VERSION);
  1         2  
  1         87  
8 1     1   1057 use File::Spec::Functions ();
  1         1007  
  1         22  
9 1     1   1001 use DirHandle;
  1         2483  
  1         29  
10 1     1   6 use Carp;
  1         2  
  1         54  
11 1     1   5 use Cwd;
  1         1  
  1         4679  
12              
13             require Exporter;
14             @ISA = qw(Exporter);
15             @EXPORT_OK = qw(get_dir);
16              
17             $VERSION = '1.00';
18              
19             ######################################################################
20             # object interface
21              
22             sub new {
23 2     2 0 78 my ($class, $path, $details, $depth) = @_;
24 2 50       11 $path = cwd unless @_ > 1;
25 2 50       8 $details = 2 unless @_ > 2;
26 2 100       10 $depth = 1000 unless @_ > 3;
27              
28 2 50       8 $path = cwd if $path eq '.';
29              
30 2         32 my $self = {
31             path => File::Spec::Functions::canonpath($path),
32             details => $details,
33             depth => $depth,
34             error => 0,
35             catch_error => 0,
36             ns_enabled => 0,
37             doctype => 0,
38             rdf_enabled => 0,
39             n3_index => '',
40             ns_uri => 'http://gingerall.org/directory/1.0/',
41             ns_prefix => 'xd',
42             encoding => 'utf-8',
43             };
44 2         7 bless $self, $class;
45 2         7 return $self;
46             }
47              
48             sub parse {
49 1     1 0 3 my $self = shift;
50              
51 1 50       14 if ($self->{details} !~ /^[123]$/) {
52 0         0 $self->doError(1,$self->{details})
53             }
54 1 50       13 if ($self->{depth} !~ /^\d+$/) {
55 0         0 $self->doError(2,$self->{depth})
56             }
57 1 50       5 if ($self->{error} == 0) {
58              
59 1         3 $self->{seq} = 1; # a sequence used for doc:Position
60              
61 1         2 eval {
62 1 50       29 chdir ($self->{path}) or die "Path $self->{path} not found!\n";
63             # turning relative paths to absolute ones
64 1         3216 $self->{path} = cwd;
65 1         39 my @dirs = File::Spec::Functions::splitdir($self->{path});
66 1         42 my $dirname = pop @dirs;
67              
68 1         20 $self->doStartDocument;
69              
70 1 50       9 if ($self->{ns_enabled}) {
71 0         0 my @attr = ();
72 0         0 my $decl = $self->_ns_declaration;
73 0         0 push @attr, [$decl => $self->{ns_uri}];
74 0 0       0 push @attr, ['xmlns:doc' =>
75             'http://gingerall.org/charlie-doc/1.0/']
76             if $self->{rdf_enabled};
77 0         0 $self->doStartElement('dirtree', \@attr);
78             } else {
79 1         12 $self->doStartElement('dirtree', undef);
80             }
81              
82 1 50       6 if ($self->{details} > 1) {
83 0         0 my @attr = ();
84 0         0 push @attr, [version => $XML::Directory::VERSION];
85            
86 0         0 $self->doStartElement('head', \@attr);
87 0         0 $self->doElement('path', undef, $self->{path});
88 0         0 $self->doElement('details', undef, $self->{details});
89 0         0 $self->doElement('depth', undef, $self->{depth});
90 0         0 $self->doElement('orderby', [[code=>$self->order_by()]], undef);
91 0         0 $self->doEndElement('head');
92             }
93            
94 1         40 my $rc = $self->_directory('', $dirname, 0);
95 1 50       4 return 0 if $rc == -1;
96            
97 1         4 $self->doEndElement('dirtree');
98 1         4 $self->doEndDocument;
99             };
100 1 50       8 if ($@) {
101 0         0 chomp $@;
102 0         0 $self->doError(3,$@);
103             }
104             }
105             }
106              
107             sub set_path {
108 0     0 0 0 my ($self, $path) = @_;
109 0 0       0 $path = cwd unless @_ > 1;
110 0         0 $self->{path} = File::Spec::Functions::canonpath($path),;
111             }
112              
113             sub set_details {
114 0     0 0 0 my ($self, $details) = @_;
115 0 0       0 $details = 2 unless @_ > 1;
116 0         0 $self->{details} = $details;
117             }
118              
119             sub set_maxdepth {
120 0     0 0 0 my ($self, $depth) = @_;
121 0 0       0 $depth = 1000 unless @_ > 1;
122 0         0 $self->{depth} = $depth;
123             }
124              
125             sub get_path {
126 0     0 0 0 my $self = shift;
127 0         0 return $self->{path};
128             }
129              
130             sub get_details {
131 1     1 0 23 my $self = shift;
132 1         4 return $self->{details};
133             }
134              
135             sub get_maxdepth {
136 1     1 0 9 my $self = shift;
137 1         3 return $self->{depth};
138             }
139              
140             sub enable_ns {
141 0     0 0 0 my $self = shift;
142 0         0 $self->{ns_enabled} = 1;
143             }
144              
145             sub disable_ns {
146 0     0 0 0 my $self = shift;
147 0         0 $self->{ns_enabled} = 0;
148             }
149              
150             sub enable_doctype {
151 0     0 0 0 my $self = shift;
152 0         0 $self->{doctype} = 1;
153             }
154              
155             sub disable_doctype {
156 0     0 0 0 my $self = shift;
157 0         0 $self->{doctype} = 0;
158             }
159              
160             sub get_ns_data {
161 0     0 0 0 my $self = shift;
162             return {
163 0         0 ns_enabled => $self->{ns_enabled},
164             ns_uri => $self->{ns_uri},
165             ns_prefix => $self->{ns_prefix},
166             };
167             }
168              
169             sub encoding {
170 0     0 0 0 my ($self, $code) = @_;
171 0 0       0 if (@_ > 1) {
172 0         0 $self->{encoding} = $code;
173             } else {
174 0         0 return $self->{encoding};
175             }
176             }
177              
178             sub error_treatment {
179 0     0 0 0 my ($self, $val) = @_;
180 0 0       0 if (@_ > 1) {
181 0 0       0 $self->{catch_error} = 0 if $val eq 'die';
182 0 0       0 $self->{catch_error} = 1 if $val eq 'warn';
183             } else {
184 0 0       0 return 'die' if $self->{catch_error} == 0;
185 0 0       0 return 'warn' if $self->{catch_error} == 1;
186             }
187             }
188              
189             sub enable_rdf {
190 0     0 0 0 my ($self, $index) = @_;
191 0         0 $self->{ns_enabled} = 1;
192 0         0 $self->{rdf_enabled} = 1;
193 0         0 $self->{n3_index} = $index;
194 0         0 eval { require RDF::Notation3; };
  0         0  
195 0         0 chomp $@;
196 0 0       0 $self->doError(5,$@) if $@;
197             }
198              
199             sub disable_rdf {
200 0     0 0 0 my $self = shift;
201 0         0 $self->{rdf_enabled} = 0;
202             }
203              
204             sub order_by {
205 3     3 0 7 my ($self, $code) = @_;
206              
207 3 50       9 if (defined($code)) {
208 0         0 $self->{'__orderby'} = $code;
209              
210             }
211              
212 3   50     31 return $self->{'__orderby'} || "df";
213             }
214              
215             ######################################################################
216             # original interface
217              
218             sub get_dir {
219            
220 1     1 0 85 require XML::Directory::String;
221 1         17 my $h = XML::Directory::String->new(@_);
222 1         7 $h->parse_dir;
223 1         2 return @{$h->{xml}};
  1         27  
224             }
225              
226             ######################################################################
227             # private procedures
228              
229             sub _directory {
230 1     1   7 my ($self, $path, $dirname, $level, $rdf_data_P, $rdf_P) = @_;
231              
232             # rdf metadata
233 1         2 my $rdf_data = 0; # RDF/N3 meta-data found or not
234 1         4 my $doc_prefix = 'doc'; # default prefix
235 1         2 my $rdf; # rdf object
236 1         2 my $stop = 0; # end of recursion controlled by meta-data
237              
238 1 50       5 if ($self->{rdf_enabled}) {
239              
240 0 0       0 if (-f $self->{n3_index}) {
241 0         0 require RDF::Notation3::PrefTriples;
242 0         0 $rdf = RDF::Notation3::PrefTriples->new();
243 0         0 eval {_try_to_parse($rdf, $self->{n3_index})};
  0         0  
244 0 0       0 if ($@) {
245 0         0 $self->doError(6,"$dirname, $@");
246 0         0 return -1;
247             } else {
248 0         0 $rdf_data = 1;
249             }
250             }
251             # parent N3 is read for uppermost directories only
252 0 0       0 if (not $rdf_data_P) {
253             # link-safe way to get a parent dir
254 0         0 my $p_n3 = $self->{path} . $path;
255 0         0 $p_n3 =~ s/[^\/\\]+$/$self->{n3_index}/;
256 0         0 $p_n3 = File::Spec::Functions::canonpath($p_n3);
257              
258 0 0       0 if (-f $p_n3) {
259 0         0 require RDF::Notation3::PrefTriples;
260 0         0 $rdf_P = RDF::Notation3::PrefTriples->new();
261 0         0 eval {$rdf_P->parse_file($p_n3)};
  0         0  
262 0 0       0 if ($@) {
263 0         0 $self->doError(6,"$dirname, $@");
264 0         0 return -1;
265             } else {
266 0         0 $rdf_data_P = 1;
267             }
268             }
269             }
270             }
271              
272 1         25 my @stat = stat '.';
273 1         7 $dirname =~ s/&/&/;
274              
275 1         6 my @attr = ([name => $dirname]);
276 1 50       10 push @attr, ['depth', $level] if $self->{details} > 1;
277 1 50       4 push @attr, ['uid', $stat[4]] if $self->{details} > 2;
278 1 50       4 push @attr, ['gid', $stat[5]] if $self->{details} > 2;
279              
280             # rdf metadata NS
281 1 50       3 if ($rdf_data) {
282 0         0 foreach (keys %{$rdf->{ns}->{$rdf->{context}}}) {
  0         0  
283 0 0       0 if ($rdf->{ns}->{$rdf->{context}}->{$_} eq
284             'http://gingerall.org/charlie-doc/1.0/') {
285 0         0 $doc_prefix = $_;
286             }
287 0         0 push @attr,
288             ["xmlns:$_" => $rdf->{ns}->{$rdf->{context}}->{$_}];
289             }
290             }
291 1 50       3 if ($rdf_data_P) {
292 0         0 foreach (keys %{$rdf_P->{ns}->{$rdf_P->{context}}}) {
  0         0  
293              
294 0 0 0     0 unless ($rdf_data and $rdf->{ns}->{$rdf->{context}}->{$_} and
      0        
295             $rdf->{ns}->{$rdf->{context}}->{$_} eq
296             $rdf_P->{ns}->{$rdf_P->{context}}->{$_}) {
297              
298             # the same prefix bound to different NS in $rdf and $rdf_P
299             # launches an error to prevent not well-formed XML
300 0 0 0     0 if ($rdf_data and $rdf->{ns}->{$rdf->{context}}->{$_} and
      0        
301             $rdf->{ns}->{$rdf->{context}}->{$_} ne
302             $rdf_P->{ns}->{$rdf_P->{context}}->{$_}) {
303 0         0 my $msg = "$_ -> $rdf->{ns}->{$rdf->{context}}->{$_}, "
304             . "$rdf_P->{ns}->{$rdf_P->{context}}->{$_} in "
305             . $self->{path} . $path . ' and its parent';
306 0         0 $self->doError(7,$msg);
307             }
308            
309 0         0 push @attr,
310             ["xmlns:$_" => $rdf_P->{ns}->{$rdf_P->{context}}->{$_}];
311             }
312             }
313             }
314              
315 1         8 $self->doStartElement('directory', \@attr);
316              
317 1 50       5 $self->doElement('path', undef, $path) if $self->{details} > 1;
318              
319 1         182 my $atime = localtime($stat[8]);
320 1         21 my $mtime = localtime($stat[9]);
321 1 50       7 $self->doElement('access-time', [[epoch => $stat[8]]], $atime)
322             if $self->{details} > 2;
323 1 50       12 $self->doElement('modify-time', [[epoch => $stat[9]]], $mtime)
324             if $self->{details} > 1;
325              
326             # rdf metadata for nested or uppermost dirs dirs
327 1 50       3 if ($self->{details} > 1) {
328 0         0 my $position_set = 0;
329 0         0 my $cnt = 0;
330 0 0       0 if ($rdf_data_P) {
331 0         0 $cnt = scalar @{$rdf_P->{triples}};
  0         0  
332 0         0 for (my $i = 0; $i < $cnt; $i++) {
333 0 0       0 if ($rdf_P->{triples}->[$i]->[0] eq "<$dirname>") {
334 0         0 $self->doElement("$doc_prefix:Position",undef,$i+1,1);
335 0         0 $position_set = 1;
336 0         0 last;
337             }
338             }
339 0         0 my $triples = $rdf_P->get_triples("<$dirname>");
340 0         0 foreach (@$triples) {
341 0         0 $_->[2] =~ s/^"(.*)"$/$1/;
342 0         0 $self->doElement($_->[1],undef,_esc($_->[2]),1);
343              
344             # looking for doc:Type = 'document'
345 0         0 $_->[1] =~ s/^([_a-zA-Z]\w*)*:/$rdf_P->{ns}->{'<>'}->{$1}/;
346 0 0 0     0 $stop = 1
347             if $_->[1] eq 'http://gingerall.org/charlie-doc/1.0/Type'
348             and $_->[2] eq 'document';
349             }
350             }
351 0 0 0     0 if ($self->{rdf_enabled} and not($position_set)) {
352 0         0 $self->_doUnknownPosition($cnt, $doc_prefix);
353             }
354             }
355              
356 1         2 foreach my $d (@{$self->_readdir()}) {
  1         15  
357              
358 8 50       144 if (-d $d) {
359              
360             # nested dirs
361 0 0       0 if ($self->{depth} > $level) {
362 0         0 $level++;
363            
364 0         0 my $path = File::Spec::Functions::catfile($path, $d);
365            
366 0 0       0 unless ($stop) {
367 0         0 my $parent_dir = $self->{path} . $path;
368 0         0 $parent_dir =~ s/[^\/\\]+$//;
369 0         0 $parent_dir = File::Spec::Functions::canonpath($parent_dir);
370            
371 0 0       0 chdir $d or croak "Cannot chdir to $d, $!\n";
372 0         0 $self->_directory($path, $d, $level, $rdf_data, $rdf);
373 0         0 chdir $parent_dir;
374              
375 0         0 $level--;
376             }
377             }
378              
379             # final dirs
380 0 0       0 if ($self->{depth} == $level) {
381            
382 0         0 my $path = File::Spec::Functions::catfile($path, $d);
383 0         0 my @stat = stat "$d";
384            
385 0         0 $d =~ s/&/&/;
386            
387 0         0 my @attr = ([name => $d]);
388 0 0       0 push @attr, ['depth', $level] if $self->{details} > 1;
389 0 0       0 push @attr, ['uid', $stat[4]] if $self->{details} > 2;
390 0 0       0 push @attr, ['gid', $stat[5]] if $self->{details} > 2;
391            
392 0 0       0 if ($self->{details} == 1) {
393 0         0 $self->doElement('directory', \@attr, undef)
394             } else {
395 0         0 $self->doStartElement('directory', \@attr);
396            
397 0         0 $self->doElement('path', undef, $path);
398 0         0 my $atime = localtime($stat[8]);
399 0         0 my $mtime = localtime($stat[9]);
400 0 0       0 $self->doElement('access-time', [[epoch => $stat[8]]], $atime)
401             if $self->{details} > 2;
402 0         0 $self->doElement('modify-time', [[epoch => $stat[9]]], $mtime);
403            
404             # rdf metadata
405 0         0 my $position_set = 0;
406 0         0 my $cnt = 0;
407 0 0       0 if ($rdf_data) {
408 0         0 $cnt = scalar @{$rdf->{triples}};
  0         0  
409 0         0 for (my $i = 0; $i < $cnt; $i++) {
410 0 0       0 if ($rdf->{triples}->[$i]->[0] eq "<$d>") {
411 0         0 $self->doElement("$doc_prefix:Position",undef,$i+1,1);
412 0         0 $position_set = 1;
413 0         0 last;
414             }
415             }
416 0         0 my $triples = $rdf->get_triples("<$d>");
417 0         0 foreach (@$triples) {
418 0         0 $_->[2] =~ s/^"(.*)"$/$1/;
419 0         0 $self->doElement($_->[1],undef,_esc($_->[2]),1);
420             }
421             }
422 0 0 0     0 if ($self->{rdf_enabled} and not($position_set)) {
423 0         0 $self->_doUnknownPosition($cnt, $doc_prefix);
424             }
425 0         0 $self->doEndElement('directory');
426             }
427             }
428             }
429              
430             else {
431             # files
432 8 50       14 unless ($stop) {
433 8 50       19 unless ($d eq $self->{n3_index}) {
434 8         24 $self->_file($d, $level, $rdf_data, $rdf, $doc_prefix);
435             }
436             }
437             }
438             }
439              
440 1         8 $self->doEndElement('directory');
441             }
442              
443             sub _readdir {
444 1     1   2 my $self = shift;
445              
446 1         12 my $path = &Cwd::getcwd();
447 1         16 my $dh = DirHandle->new($path);
448              
449 1 50       173 if (! $dh) {
450 0         0 carp $!;
451 0         0 return [];
452             }
453              
454 1         3 my @dirs = ();
455 1         3 my @files = ();
456            
457 1         7 foreach ($dh->read()) {
458 10 100       86 next if $_ =~ /^(\.{1,2})$/;
459 8 50       193 (-d "$path/$_") ? push @dirs, $_ : push @files, $_;
460             }
461              
462 1 50       15 if ($self->order_by() eq "fd") {
    50          
    50          
463 0         0 return [sort(@files),sort(@dirs)];
464             }
465            
466             elsif ($self->order_by() eq "a") {
467 0         0 return [sort(@files,@dirs)];
468             }
469              
470             elsif ($self->order_by() eq "z") {
471 0         0 return [sort {$b cmp $a} (@files,@dirs)];
  0         0  
472             }
473              
474             else {
475 1         29 return [sort(@dirs),sort(@files)];
476             }
477             }
478              
479             sub _file($$$$) {
480 8     8   17 my ($self, $name, $level, $rdf_data, $rdf, $doc_prefix) = @_;
481              
482 8         77 my @stat = stat $name;
483              
484 8         15 my @attr = ();
485 8         15 push @attr, [name => _esc($name)];
486 8 50       23 push @attr, [uid => $stat[4]] if $self->{details} > 2;
487 8 50       16 push @attr, [gid => $stat[5]] if $self->{details} > 2;
488              
489 8 50       18 if ($self->{details} == 1) {
490 8         26 $self->doElement('file', \@attr, undef)
491             } else {
492 0         0 $self->doStartElement('file', \@attr);
493              
494 0         0 my $mode;
495 0 0       0 if (-r $name) {$mode = 'r' }else {$mode = '-'}
  0         0  
  0         0  
496 0 0       0 if (-w $name) {$mode .= 'w' }else {$mode .= '-'}
  0         0  
  0         0  
497 0 0       0 if (-x $name) {$mode .= 'x' }else {$mode .= '-'}
  0         0  
  0         0  
498 0 0       0 $self->doElement('mode', [[code => $stat[2]]], $mode)
499             if $self->{details} > 1;
500 0 0       0 $self->doElement('size', [[unit => 'bytes']], $stat[7])
501             if $self->{details} > 1;
502              
503 0         0 my $atime = localtime($stat[8]);
504 0         0 my $mtime = localtime($stat[9]);
505 0 0       0 $self->doElement('access-time', [[epoch => $stat[8]]], $atime)
506             if $self->{details} > 2;
507 0 0       0 $self->doElement('modify-time', [[epoch => $stat[9]]], $mtime)
508             if $self->{details} > 1;
509              
510             # rdf metadata
511 0         0 my $position_set = 0;
512 0         0 my $cnt = 0;
513 0 0       0 if ($rdf_data) {
514 0         0 $cnt = scalar @{$rdf->{triples}};
  0         0  
515 0         0 for (my $i = 0; $i < $cnt; $i++) {
516 0 0       0 if ($rdf->{triples}->[$i]->[0] eq "<$name>") {
517 0         0 $self->doElement("$doc_prefix:Position",undef,$i+1,1);
518 0         0 $position_set = 1;
519 0         0 last;
520             }
521             }
522 0         0 my $triples = $rdf->get_triples("<$name>");
523 0         0 foreach (@$triples) {
524 0         0 $_->[2] =~ s/^"(.*)"$/$1/;
525 0         0 $self->doElement($_->[1],undef,_esc($_->[2]),1);
526             }
527             }
528 0 0 0     0 if ($self->{rdf_enabled} and not($position_set)) {
529 0         0 $self->_doUnknownPosition($cnt, $doc_prefix);
530             }
531 0         0 $self->doEndElement('file');
532             }
533             }
534              
535             sub _ns_declaration {
536 0     0   0 my $self = shift;
537            
538 0 0       0 return '' unless $self->{ns_enabled};
539 0 0       0 return $self->{ns_prefix} ? "xmlns:$self->{ns_prefix}" : 'xmlns';
540             }
541              
542             sub _doUnknownPosition {
543 0     0   0 my ($self, $cnt, $prefix) = @_;
544            
545 0         0 $self->doElement("$prefix:Position", undef, $cnt + $self->{seq}, 1);
546 0         0 $self->{seq}++;
547             }
548              
549             sub _esc {
550 8     8   9 my $str = shift;
551              
552 8         10 $str =~ s/&/&/g;
553 8         11 $str =~ s/
554 8         9 $str =~ s/>/>/g;
555 8         23 return $str;
556             }
557              
558             sub _try_to_parse {
559 0     0     my ($rdf, $path) = @_;
560 0           my $done = 0;
561 0           my $count = 0;
562              
563 0   0       until ($done or $count == 10) {
564 0           eval {$rdf->parse_file("$path")};
  0            
565 0 0         unless ($@) {
566 0           $done = 1;
567             } else {
568 0           select(undef, undef, undef, 0.02);
569 0           $count++;
570             }
571             }
572 0 0         die $@ if $@;
573             }
574              
575             sub _msg {
576 0     0     my ($self, $no) = @_;
577              
578 0           my %msg = (
579             1 => 'details value invalid',
580             2 => 'depth value invalid',
581             3 => 'parse error',
582             4 => 'input source not supported',
583             5 => 'required module not found',
584             6 => 'RDF data parse error',
585             7 => 'prefix bound to 2 namespaces',
586             8 => 'content handler not found',
587             );
588              
589 0           return $msg{$no};
590             }
591              
592             1;
593              
594             __END__