/[webpac2]/trunk/lib/WebPAC/Normalize.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Diff of /trunk/lib/WebPAC/Normalize.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 14 by dpavlin, Sun Jul 17 00:04:25 2005 UTC revision 538 by dpavlin, Thu Jun 29 15:29:19 2006 UTC
# Line 1  Line 1 
1  package WebPAC::Normalize;  package WebPAC::Normalize;
2    use Exporter 'import';
3    @EXPORT = qw/
4            _set_rec _set_lookup
5            _get_ds _clean_ds
6    
7            tag search display
8            rec1 rec2 rec
9            regex prefix suffix surround
10            first lookup join_with
11    /;
12    
13  use warnings;  use warnings;
14  use strict;  use strict;
15    
16    #use base qw/WebPAC::Common/;
17  use Data::Dumper;  use Data::Dumper;
 use Storable;  
18    
19  =head1 NAME  =head1 NAME
20    
21  WebPAC::Normalize - normalisation of source file  WebPAC::Normalize - describe normalisaton rules using sets
22    
23  =head1 VERSION  =head1 VERSION
24    
25  Version 0.01  Version 0.05
26    
27  =cut  =cut
28    
29  our $VERSION = '0.01';  our $VERSION = '0.05';
30    
31  =head1 SYNOPSIS  =head1 SYNOPSIS
32    
33  This package contains code that could be helpful in implementing different  This module uses C<conf/normalize/*.pl> files to perform normalisation
34  normalisation front-ends.  from input records using perl functions which are specialized for set
35    processing.
36    
37    Sets are implemented as arrays, and normalisation file is valid perl, which
38    means that you check it's validity before running WebPAC using
39    C<perl -c normalize.pl>.
40    
41    Normalisation can generate multiple output normalized data. For now, supported output
42    types (on the left side of definition) are: C<tag>, C<display> and C<search>.
43    
44  =head1 FUNCTIONS  =head1 FUNCTIONS
45    
46  =head2 new  Functions which start with C<_> are private and used by WebPAC internally.
47    All other functions are available for use within normalisation rules.
48    
49    =head2 data_structure
50    
51  Create new normalisation object  Return data structure
52    
53    my $n = new WebPAC::Normalize::Something(    my $ds = WebPAC::Normalize::data_structure(
54          cache_data_structure => './cache/ds/',          lookup => $lookup->lookup_hash,
55          lookup_regex => $lookup->regex,          row => $row,
56            rules => $normalize_pl_config,
57    );    );
58    
59  Optional parameter C<cache_data_structure> defines path to directory  This function will B<die> if normalizastion can't be evaled.
 in which cache file for C<data_structure> call will be created.  
60    
61  Recommended parametar C<lookup_regex> is used to enable parsing of lookups  Since this function isn't exported you have to call it with
62  in structures.  C<WebPAC::Normalize::data_structure>.
63    
64  =cut  =cut
65    
66  sub new {  sub data_structure {
67          my $class = shift;          my $arg = {@_};
         my $self = {@_};  
         bless($self, $class);  
68    
69          $self->setup_cache_dir( $self->{'cache_data_structure'} );          die "need row argument" unless ($arg->{row});
70            die "need normalisation argument" unless ($arg->{rules});
71    
72          $self ? return $self : return undef;          no strict 'subs';
73            _set_lookup( $arg->{lookup} );
74            _set_rec( $arg->{row} );
75            _clean_ds();
76            eval "$arg->{rules}";
77            die "error evaling $arg->{rules}: $@\n" if ($@);
78            return _get_ds();
79  }  }
80    
81  =head2 setup_cache_dir  =head2 _set_rec
   
 Check if specified cache directory exist, and if not, disable caching.  
82    
83   $setup_cache_dir('./cache/ds/');  Set current record hash
84    
85  If you pass false or zero value to this function, it will disable    _set_rec( $rec );
 cacheing.  
86    
87  =cut  =cut
88    
89  sub setup_cache_dir {  my $rec;
         my $self = shift;  
   
         my $dir = shift;  
90    
91          my $log = $self->_get_logger();  sub _set_rec {
92            $rec = shift or die "no record hash";
         if ($dir) {  
                 my $msg;  
                 if (! -e $dir) {  
                         $msg = "doesn't exist";  
                 } elsif (! -d $dir) {  
                         $msg = "is not directory";  
                 } elsif (! -w $dir) {  
                         $msg = "not writable";  
                 }  
   
                 if ($msg) {  
                         undef $self->{'cache_data_structure'};  
                         $log->warn("cache_data_structure $dir $msg, disabling...");  
                 } else {  
                         $log->debug("using cache dir $dir");  
                 }  
         } else {  
                 $log->debug("disabling cache");  
                 undef $self->{'cache_data_structure'};  
         }  
93  }  }
94    
95    =head2 _get_ds
96    
97  =head2 data_structure  Return hash formatted as data structure
   
 Create in-memory data structure which represents normalized layout from  
 C<conf/normalize/*.xml>.  
   
 This structures are used to produce output.  
98    
99   my @ds = $webpac->data_structure($rec);    my $ds = _get_ds();
   
 B<Note: historical oddity follows>  
   
 This method will also set C<< $webpac->{'currnet_filename'} >> if there is  
 C<< <filename> >> tag and C<< $webpac->{'headline'} >> if there is  
 C<< <headline> >> tag.  
100    
101  =cut  =cut
102    
103  sub data_structure {  my $out;
         my $self = shift;  
   
         my $log = $self->_get_logger();  
   
         my $rec = shift;  
         $log->logconfess("need HASH as first argument!") if ($rec !~ /HASH/o);  
   
         my $cache_file;  
   
         if (my $cache_path = $self->{'cache_data_structure'}) {  
                 my $id = $rec->{'000'};  
                 $id = $rec->{'000'}->[0] if ($id =~ m/^ARRAY/o);  
                 unless (defined($id)) {  
                         $log->warn("Can't use cache_data_structure on records without unique identifier in field 000");  
                         undef $self->{'cache_data_structure'};  
                 } else {  
                         $cache_file = "$cache_path/$id";  
                         if (-r $cache_file) {  
                                 my $ds_ref = retrieve($cache_file);  
                                 if ($ds_ref) {  
                                         $log->debug("cache hit: $cache_file");  
                                         my $ok = 1;  
                                         foreach my $f (qw(current_filename headline)) {  
                                                 if ($ds_ref->{$f}) {  
                                                         $self->{$f} = $ds_ref->{$f};  
                                                 } else {  
                                                         $ok = 0;  
                                                 }  
                                         };  
                                         if ($ok && $ds_ref->{'ds'}) {  
                                                 return @{ $ds_ref->{'ds'} };  
                                         } else {  
                                                 $log->warn("cache_data_structure $cache_path corrupt. Use rm $cache_path/* to re-create it on next run!");  
                                                 undef $self->{'cache_data_structure'};  
                                         }  
                                 }  
                         }  
                 }  
         }  
   
         undef $self->{'currnet_filename'};  
         undef $self->{'headline'};  
   
         my @sorted_tags;  
         if ($self->{tags_by_order}) {  
                 @sorted_tags = @{$self->{tags_by_order}};  
         } else {  
                 @sorted_tags = sort { $self->_sort_by_order } keys %{$self->{'import_xml'}->{'indexer'}};  
                 $self->{tags_by_order} = \@sorted_tags;  
         }  
104    
105          my @ds;  sub _get_ds {
106            return $out;
107    }
108    
109          $log->debug("tags: ",sub { join(", ",@sorted_tags) });  =head2 _clean_ds
110    
111          foreach my $field (@sorted_tags) {  Clean data structure hash for next record
112    
113                  my $row;    _clean_ds();
114    
115  #print "field $field [",$self->{'tag'},"] = ",Dumper($self->{'import_xml'}->{'indexer'}->{$field}->{$self->{'tag'}});  =cut
116    
117                  foreach my $tag (@{$self->{'import_xml'}->{'indexer'}->{$field}->{$self->{'tag'}}}) {  sub _clean_ds {
118                          my $format = $tag->{'value'} || $tag->{'content'};          $out = undef;
119    }
120    
121                          $log->debug("format: $format");  =head2 _set_lookup
122    
123                          my @v;  Set current lookup hash
                         if ($self->{'lookup_regex'} && $format =~ $self->{'lookup_regex'}) {  
                                 @v = $self->fill_in_to_arr($rec,$format);  
                         } else {  
                                 @v = $self->parse_to_arr($rec,$format);  
                         }  
                         next if (! @v);  
124    
125                          if ($tag->{'sort'}) {    _set_lookup( $lookup );
                                 @v = $self->sort_arr(@v);  
                         }  
126    
127                          # use format?  =cut
                         if ($tag->{'format_name'}) {  
                                 @v = map { $self->apply_format($tag->{'format_name'},$tag->{'format_delimiter'},$_) } @v;  
                         }  
128    
129                          if ($field eq 'filename') {  my $lookup;
                                 $self->{'current_filename'} = join('',@v);  
                                 $log->debug("filename: ",$self->{'current_filename'});  
                         } elsif ($field eq 'headline') {  
                                 $self->{'headline'} .= join('',@v);  
                                 $log->debug("headline: ",$self->{'headline'});  
                                 next; # don't return headline in data_structure!  
                         }  
130    
131                          # delimiter will join repeatable fields  sub _set_lookup {
132                          if ($tag->{'delimiter'}) {          $lookup = shift;
133                                  @v = ( join($tag->{'delimiter'}, @v) );  }
                         }  
134    
135                          # default types  =head2 tag
                         my @types = qw(display swish);  
                         # override by type attribute  
                         @types = ( $tag->{'type'} ) if ($tag->{'type'});  
   
                         foreach my $type (@types) {  
                                 # append to previous line?  
                                 $log->debug("type: $type ",sub { join(" ",@v) }, $row->{'append'} || 'no append');  
                                 if ($tag->{'append'}) {  
   
                                         # I will delimit appended part with  
                                         # delimiter (or ,)  
                                         my $d = $tag->{'delimiter'};  
                                         # default delimiter  
                                         $d ||= " ";  
   
                                         my $last = pop @{$row->{$type}};  
                                         $d = "" if (! $last);  
                                         $last .= $d . join($d, @v);  
                                         push @{$row->{$type}}, $last;  
   
                                 } else {  
                                         push @{$row->{$type}}, @v;  
                                 }  
                         }  
136    
137    Define new tag for I<search> and I<display>.
138    
139                  }    tag('Title', rec('200','a') );
140    
                 if ($row) {  
                         $row->{'tag'} = $field;  
141    
142                          # TODO: name_sigular, name_plural  =cut
                         my $name = $self->{'import_xml'}->{'indexer'}->{$field}->{'name'};  
                         $row->{'name'} = $name ? $self->_x($name) : $field;  
   
                         # post-sort all values in field  
                         if ($self->{'import_xml'}->{'indexer'}->{$field}->{'sort'}) {  
                                 $log->warn("sort at field tag not implemented");  
                         }  
143    
144                          push @ds, $row;  sub tag {
145            my $name = shift or die "tag needs name as first argument";
146            my @o = grep { defined($_) && $_ ne '' } @_;
147            return unless (@o);
148            $out->{$name}->{tag} = $name;
149            $out->{$name}->{search} = \@o;
150            $out->{$name}->{display} = \@o;
151    }
152    
153                          $log->debug("row $field: ",sub { Dumper($row) });  =head2 display
                 }  
154    
155          }  Define tag just for I<display>
156    
157          if ($cache_file) {    @v = display('Title', rec('200','a') );
                 store {  
                         ds => \@ds,  
                         current_filename => $self->{'current_filename'},  
                         headline => $self->{'headline'},  
                 }, $cache_file;  
                 $log->debug("created storable cache file $cache_file");  
         }  
158    
159          return @ds;  =cut
160    
161    sub display {
162            my $name = shift or die "display needs name as first argument";
163            my @o = grep { defined($_) && $_ ne '' } @_;
164            return unless (@o);
165            $out->{$name}->{tag} = $name;
166            $out->{$name}->{display} = \@o;
167  }  }
168    
169  =head2 apply_format  =head2 search
   
 Apply format specified in tag with C<format_name="name"> and  
 C<format_delimiter=";;">.  
170    
171   my $text = $webpac->apply_format($format_name,$format_delimiter,$data);  Prepare values just for I<search>
172    
173  Formats can contain C<lookup{...}> if you need them.    @v = search('Title', rec('200','a') );
174    
175  =cut  =cut
176    
177  sub apply_format {  sub search {
178          my $self = shift;          my $name = shift or die "search needs name as first argument";
179            my @o = grep { defined($_) && $_ ne '' } @_;
180          my ($name,$delimiter,$data) = @_;          return unless (@o);
181            $out->{$name}->{tag} = $name;
182          my $log = $self->_get_logger();          $out->{$name}->{search} = \@o;
183    }
184    
185          if (! $self->{'import_xml'}->{'format'}->{$name}) {  =head2 rec1
                 $log->warn("<format name=\"$name\"> is not defined in ",$self->{'import_xml_file'});  
                 return $data;  
         }  
186    
187          $log->warn("no delimiter for format $name") if (! $delimiter);  Return all values in some field
188    
189          my $format = $self->_x($self->{'import_xml'}->{'format'}->{$name}->{'content'}) || $log->logdie("can't find format '$name'");    @v = rec1('200')
190    
191          my @data = split(/\Q$delimiter\E/, $data);  TODO: order of values is probably same as in source data, need to investigate that
192    
193          my $out = sprintf($format, @data);  =cut
         $log->debug("using format $name [$format] on $data to produce: $out");  
194    
195          if ($self->{'lookup_regex'} && $out =~ $self->{'lookup_regex'}) {  sub rec1 {
196                  return $self->lookup($out);          my $f = shift;
197          } else {          return unless (defined($rec) && defined($rec->{$f}));
198                  return $out;          if (ref($rec->{$f}) eq 'ARRAY') {
199                    return map {
200                            if (ref($_) eq 'HASH') {
201                                    values %{$_};
202                            } else {
203                                    $_;
204                            }
205                    } @{ $rec->{$f} };
206            } elsif( defined($rec->{$f}) ) {
207                    return $rec->{$f};
208          }          }
   
209  }  }
210    
211  =head2 parse  =head2 rec2
212    
213  Perform smart parsing of string, skipping delimiters for fields which aren't  Return all values in specific field and subfield
 defined. It can also eval code in format starting with C<eval{...}> and  
 return output or nothing depending on eval code.  
214    
215   my $text = $webpac->parse($rec,'eval{"v901^a" eq "Deskriptor"}descriptor: v250^a', $i);    @v = rec2('200','a')
216    
217  =cut  =cut
218    
219  sub parse {  sub rec2 {
220          my $self = shift;          my $f = shift;
221            return unless (defined($rec && $rec->{$f}));
222          my ($rec, $format_utf8, $i) = @_;          my $sf = shift;
223            return map { $_->{$sf} } grep { ref($_) eq 'HASH' && $_->{$sf} } @{ $rec->{$f} };
224          return if (! $format_utf8);  }
   
         my $log = $self->_get_logger();  
   
         $log->logconfess("need HASH as first argument!") if ($rec !~ /HASH/o);  
   
         $i = 0 if (! $i);  
   
         my $format = $self->_x($format_utf8) || $log->logconfess("can't convert '$format_utf8' from UTF-8 to ",$self->{'code_page'});  
   
         my @out;  
   
         $log->debug("format: $format");  
   
         my $eval_code;  
         # remove eval{...} from beginning  
         $eval_code = $1 if ($format =~ s/^eval{([^}]+)}//s);  
   
         my $filter_name;  
         # remove filter{...} from beginning  
         $filter_name = $1 if ($format =~ s/^filter{([^}]+)}//s);  
   
         my $prefix;  
         my $all_found=0;  
   
         while ($format =~ s/^(.*?)(v|s)(\d+)(?:\^(\w))?//s) {  
   
                 my $del = $1 || '';  
                 $prefix ||= $del if ($all_found == 0);  
   
                 # repeatable index  
                 my $r = $i;  
                 $r = 0 if (lc("$2") eq 's');  
   
                 my $found = 0;  
                 my $tmp = $self->get_data(\$rec,$3,$4,$r,\$found);  
   
                 if ($found) {  
                         push @out, $del;  
                         push @out, $tmp;  
                         $all_found += $found;  
                 }  
         }  
   
         return if (! $all_found);  
225    
226          my $out = join('',@out);  =head2 rec
227    
228          if ($out) {  syntaxtic sugar for
                 # add rest of format (suffix)  
                 $out .= $format;  
229    
230                  # add prefix if not there    @v = rec('200')
231                  $out = $prefix . $out if ($out !~ m/^\Q$prefix\E/);    @v = rec('200','a')
232    
233                  $log->debug("result: $out");  =cut
         }  
234    
235          if ($eval_code) {  sub rec {
236                  my $eval = $self->fill_in($rec,$eval_code,$i) || return;          if ($#_ == 0) {
237                  $log->debug("about to eval{$eval} format: $out");                  return rec1(@_);
238                  return if (! $self->_eval($eval));          } elsif ($#_ == 1) {
239                    return rec2(@_);
240          }          }
           
         if ($filter_name && $self->{'filter'}->{$filter_name}) {  
                 $log->debug("about to filter{$filter_name} format: $out");  
                 $out = $self->{'filter'}->{$filter_name}->($out);  
                 return unless(defined($out));  
                 $log->debug("filter result: $out");  
         }  
   
         return $out;  
241  }  }
242    
243  =head2 parse_to_arr  =head2 regex
244    
245  Similar to C<parse>, but returns array of all repeatable fields  Apply regex to some or all values
246    
247   my @arr = $webpac->parse_to_arr($rec,'v250^a');    @v = regex( 's/foo/bar/g', @v );
248    
249  =cut  =cut
250    
251  sub parse_to_arr {  sub regex {
252          my $self = shift;          my $r = shift;
253            my @out;
254          my ($rec, $format_utf8) = @_;          #warn "r: $r\n",Dumper(\@_);
255            foreach my $t (@_) {
256          my $log = $self->_get_logger();                  next unless ($t);
257                    eval "\$t =~ $r";
258          $log->logconfess("need HASH as first argument!") if ($rec !~ /HASH/o);                  push @out, $t if ($t && $t ne '');
         return if (! $format_utf8);  
   
         my $i = 0;  
         my @arr;  
   
         while (my $v = $self->parse($rec,$format_utf8,$i++)) {  
                 push @arr, $v;  
259          }          }
260            return @out;
         $log->debug("format '$format_utf8' returned ",--$i," elements: ", sub { join(" | ",@arr) }) if (@arr);  
   
         return @arr;  
261  }  }
262    
263  =head2 fill_in_to_arr  =head2 prefix
264    
265  Similar to C<fill_in>, but returns array of all repeatable fields. Usable  Prefix all values with a string
 for fields which have lookups, so they shouldn't be parsed but rather  
 C<fill_id>ed.  
266    
267   my @arr = $webpac->fill_in_to_arr($rec,'[v900];;[v250^a]');    @v = prefix( 'my_', @v );
268    
269  =cut  =cut
270    
271  sub fill_in_to_arr {  sub prefix {
272          my $self = shift;          my $p = shift or die "prefix needs string as first argument";
273            return map { $p . $_ } grep { defined($_) } @_;
274          my ($rec, $format_utf8) = @_;  }
275    
276          my $log = $self->_get_logger();  =head2 suffix
277    
278          $log->logconfess("need HASH as first argument!") if ($rec !~ /HASH/o);  suffix all values with a string
         return if (! $format_utf8);  
279    
280          my $i = 0;    @v = suffix( '_my', @v );
         my @arr;  
281    
282          while (my @v = $self->fill_in($rec,$format_utf8,$i++)) {  =cut
                 push @arr, @v;  
         }  
   
         $log->debug("format '$format_utf8' returned ",--$i," elements: ", sub { join(" | ",@arr) }) if (@arr);  
283    
284          return @arr;  sub suffix {
285            my $s = shift or die "suffix needs string as first argument";
286            return map { $_ . $s } grep { defined($_) } @_;
287  }  }
288    
289  =head2 sort_arr  =head2 surround
290    
291  Sort array ignoring case and html in data  surround all values with a two strings
292    
293   my @sorted = $webpac->sort_arr(@unsorted);    @v = surround( 'prefix_', '_suffix', @v );
294    
295  =cut  =cut
296    
297  sub sort_arr {  sub surround {
298          my $self = shift;          my $p = shift or die "surround need prefix as first argument";
299            my $s = shift or die "surround needs suffix as second argument";
300          my $log = $self->_get_logger();          return map { $p . $_ . $s } grep { defined($_) } @_;
   
         # FIXME add Schwartzian Transformation?  
   
         my @sorted = sort {  
                 $a =~ s#<[^>]+/*>##;  
                 $b =~ s#<[^>]+/*>##;  
                 lc($b) cmp lc($a)  
         } @_;  
         $log->debug("sorted values: ",sub { join(", ",@sorted) });  
   
         return @sorted;  
301  }  }
302    
303    =head2 first
304    
305  =head2 _sort_by_order  Return first element
306    
307  Sort xml tags data structure accoding to C<order=""> attribute.    $v = first( @v );
308    
309  =cut  =cut
310    
311  sub _sort_by_order {  sub first {
312          my $self = shift;          my $r = shift;
313            return $r;
         my $va = $self->{'import_xml'}->{'indexer'}->{$a}->{'order'} ||  
                 $self->{'import_xml'}->{'indexer'}->{$a};  
         my $vb = $self->{'import_xml'}->{'indexer'}->{$b}->{'order'} ||  
                 $self->{'import_xml'}->{'indexer'}->{$b};  
   
         return $va <=> $vb;  
314  }  }
315    
316  =head2 _x  =head2 lookup
   
 Convert strings from C<conf/normalize> encoding into application specific  
 (optinally specified using C<code_page> to C<new> constructor.  
317    
318   my $text = $n->_x('normalize text string');  Consult lookup hashes for some value
319    
320  This is a stub so that other modules doesn't have to implement it.    @v = lookup( $v );
321      @v = lookup( @v );
322    
323  =cut  =cut
324    
325  sub _x {  sub lookup {
326          my $self = shift;          my $k = shift or return;
327          return shift;          return unless (defined($lookup->{$k}));
328            if (ref($lookup->{$k}) eq 'ARRAY') {
329                    return @{ $lookup->{$k} };
330            } else {
331                    return $lookup->{$k};
332            }
333  }  }
334    
335    =head2 join_with
336    
337  =head1 AUTHOR  Joins walues with some delimiter
   
 Dobrica Pavlinusic, C<< <dpavlin@rot13.org> >>  
   
 =head1 COPYRIGHT & LICENSE  
338    
339  Copyright 2005 Dobrica Pavlinusic, All Rights Reserved.    $v = join_with(", ", @v);
   
 This program is free software; you can redistribute it and/or modify it  
 under the same terms as Perl itself.  
340    
341  =cut  =cut
342    
343  1; # End of WebPAC::DB  sub join_with {
344            my $d = shift;
345            return join($d, grep { defined($_) && $_ ne '' } @_);
346    }
347    
348    # END
349    1;

Legend:
Removed from v.14  
changed lines
  Added in v.538

  ViewVC Help
Powered by ViewVC 1.1.26