fix dump (ugly, needs re-visiting)
[webpac2] / lib / WebPAC / Input.pm
index f4d9eb8..21bea8f 100644 (file)
@@ -7,8 +7,8 @@ use blib;
 
 use WebPAC::Common;
 use base qw/WebPAC::Common/;
-use Text::Iconv;
 use Data::Dumper;
+use Encode qw/from_to/;
 
 =head1 NAME
 
@@ -16,11 +16,11 @@ WebPAC::Input - read different file formats into WebPAC
 
 =head1 VERSION
 
-Version 0.09
+Version 0.13
 
 =cut
 
-our $VERSION = '0.09';
+our $VERSION = '0.13';
 
 =head1 SYNOPSIS
 
@@ -98,28 +98,16 @@ sub new {
        $log->logconfess("lookup argument is not suppored any more. rewrite call to lookup_ref") if ($self->{lookup});
 
        $log->logconfess("specify low-level file format module") unless ($self->{module});
-       my $module = $self->{module};
-       $module =~ s#::#/#g;
-       $module .= '.pm';
-       $log->debug("require low-level module $self->{module} from $module");
+       my $module_path = $self->{module};
+       $module_path =~ s#::#/#g;
+       $module_path .= '.pm';
+       $log->debug("require low-level module $self->{module} from $module_path");
 
-       require $module;
-       #eval $self->{module} .'->import';
+       require $module_path;
 
        # check if required subclasses are implemented
-       foreach my $subclass (qw/open_db fetch_rec init/) {
-               my $n = $self->{module} . '::' . $subclass;
-               if (! defined &{ $n }) {
-                       my $missing = "missing $subclass in $self->{module}";
-                       $self->{$subclass} = sub { $log->logwarn($missing) };
-               } else {
-                       $self->{$subclass} = \&{ $n };
-               }
-       }
-
-       if ($self->{init}) {
-               $log->debug("calling init");
-               $self->{init}->($self, @_);
+       foreach my $subclass (qw/open_db fetch_rec init dump_rec/) {
+               # FIXME
        }
 
        $self->{'encoding'} ||= 'ISO-8859-2';
@@ -159,22 +147,22 @@ This function will read whole database in memory and produce lookups.
 
  $input->open(
        path => '/path/to/database/file',
-       code_page => '852',
+       code_page => 'cp852',
        limit => 500,
        offset => 6000,
-       lookup => $lookup_obj,
        stats => 1,
-       lookup_ref => sub {
-               my ($k,$v) = @_;
-               # store lookup $k => $v
+       lookup_coderef => sub {
+               my $rec = shift;
+               # store lookups
        },
        modify_records => {
                900 => { '^a' => { ' : ' => '^b' } },
                901 => { '*' => { '^b' => ' ; ' } },
        },
+       modify_file => 'conf/modify/mapping.map',
  );
 
-By default, C<code_page> is assumed to be C<852>.
+By default, C<code_page> is assumed to be C<cp852>.
 
 C<offset> is optional parametar to position at some offset before reading from database.
 
@@ -182,13 +170,16 @@ C<limit> is optional parametar to read just C<limit> records from database
 
 C<stats> create optional report about usage of fields and subfields
 
-C<lookup_coderef> is closure to call when adding C<< key => 'value' >> combinations to
-lookup.
+C<lookup_coderef> is closure to called to save data into lookups
 
 C<modify_records> specify mapping from subfields to delimiters or from
 delimiters to subfields, as well as oprations on fields (if subfield is
 defined as C<*>.
 
+C<modify_file> is alternative for C<modify_records> above which preserves order and offers
+(hopefully) simplier sintax than YAML or perl (see L</modify_file_regex>). This option
+overrides C<modify_records> if both exists for same input.
+
 Returns size of database, regardless of C<offset> and C<limit>
 parametars, see also C<size>.
 
@@ -204,8 +195,10 @@ sub open {
        $log->logconfess("lookup_coderef must be CODE, not ",ref($arg->{lookup_coderef}))
                if ($arg->{lookup_coderef} && ref($arg->{lookup_coderef}) ne 'CODE');
 
+       $log->debug( $arg->{lookup_coderef} ? '' : 'not ', "using lookup_coderef");
+
        $log->logcroak("need path") if (! $arg->{'path'});
-       my $code_page = $arg->{'code_page'} || '852';
+       my $code_page = $arg->{'code_page'} || 'cp852';
 
        # store data in object
        $self->{'input_code_page'} = $code_page;
@@ -213,9 +206,6 @@ sub open {
                $self->{$v} = $arg->{$v} if ($arg->{$v});
        }
 
-       # create Text::Iconv object
-       $self->{iconv} = Text::Iconv->new($code_page,$self->{'encoding'});      ## FIXME remove!
-
        my $filter_ref;
        my $recode_regex;
        my $recode_map;
@@ -238,46 +228,37 @@ sub open {
 
        }
 
-       my $rec_regex = $self->modify_record_regexps(%{ $arg->{modify_records} });
-       $log->debug("rec_regex: ", Dumper($rec_regex));
-
-       my ($db, $size) = $self->{open_db}->( $self, 
-               path => $arg->{path},
-               filter => sub {
-                               my ($l,$f_nr) = @_;
-                               return unless defined($l);
-
-                               ## FIXME remove iconv!
-                               $l = $self->{iconv}->convert($l) if ($self->{iconv});
-       
-                               $l =~ s/($recode_regex)/$recode_map->{$1}/g if ($recode_regex && $recode_map);
-
-                               ## FIXME remove this warning when we are sure that none of API is calling
-                               ## this wrongly
-                               warn "filter called without field number" unless ($f_nr);
-
-                               return $l unless ($rec_regex && $f_nr);
+       my $rec_regex;
+       if (my $p = $arg->{modify_file}) {
+               $log->debug("using modify_file $p");
+               $rec_regex = $self->modify_file_regexps( $p );
+       } elsif (my $h = $arg->{modify_records}) {
+               $log->debug("using modify_records ", Dumper( $h ));
+               $rec_regex = $self->modify_record_regexps(%{ $h });
+       }
+       $log->debug("rec_regex: ", Dumper($rec_regex)) if ($rec_regex);
 
-                               # apply regexps
-                               if ($rec_regex && defined($rec_regex->{$f_nr})) {
-                                       $log->logconfess("regexps->{$f_nr} must be ARRAY") if (ref($rec_regex->{$f_nr}) ne 'ARRAY');
-                                       my $c = 0;
-                                       foreach my $r (@{ $rec_regex->{$f_nr} }) {
-                                               while ( eval '$l =~ ' . $r ) { $c++ };
-                                       }
-                                       warn "## field $f_nr triggered $c regexpes\n" if ($c && $self->{debug});
-                               }
+       my $class = $self->{module} || $log->logconfess("can't get low-level module name!");
 
-                               return $l;
-               },
+       my $ll_db = $class->new(
+               path => $arg->{path},
+#              filter => sub {
+#                      my ($l,$f_nr) = @_;
+#                      return unless defined($l);
+#                      from_to($l, $code_page, $self->{'encoding'});
+#                      $l =~ s/($recode_regex)/$recode_map->{$1}/g if ($recode_regex && $recode_map);
+#                      return $l;
+#              },
                %{ $arg },
        );
 
-       unless (defined($db)) {
+       unless (defined($ll_db)) {
                $log->logwarn("can't open database $arg->{path}, skipping...");
                return;
        }
 
+       my $size = $ll_db->size;
+
        unless ($size) {
                $log->logwarn("no records in database $arg->{path}, skipping...");
                return;
@@ -309,7 +290,34 @@ sub open {
 
                $log->debug("position: $pos\n");
 
-               my $rec = $self->{fetch_rec}->($self, $db, $pos );
+               my $rec = $ll_db->fetch_rec($pos, sub {
+                               my ($l,$f_nr) = @_;
+#                              return unless defined($l);
+#                              return $l unless ($rec_regex && $f_nr);
+
+                               $log->debug("-=> $f_nr ## $l");
+
+                               # codepage conversion and recode_regex
+                               from_to($l, $code_page, $self->{'encoding'});
+                               $l =~ s/($recode_regex)/$recode_map->{$1}/g if ($recode_regex && $recode_map);
+
+                               # apply regexps
+                               if ($rec_regex && defined($rec_regex->{$f_nr})) {
+                                       $log->logconfess("regexps->{$f_nr} must be ARRAY") if (ref($rec_regex->{$f_nr}) ne 'ARRAY');
+                                       my $c = 0;
+                                       foreach my $r (@{ $rec_regex->{$f_nr} }) {
+                                               my $old_l = $l;
+                                               eval '$l =~ ' . $r;
+                                               if ($old_l ne $l) {
+                                                       $log->debug("REGEX on $f_nr eval \$l =~ $r\n## old l: [$old_l]\n## new l: [$l]");
+                                               }
+                                               $log->error("error applying regex: $r") if ($@);
+                                       }
+                               }
+
+                               $log->debug("<=- $f_nr ## $l");
+                               return $l;
+               });
 
                $log->debug(sub { Dumper($rec) });
 
@@ -331,6 +339,9 @@ sub open {
                # update counters for statistics
                if ($self->{stats}) {
 
+                       # fetch clean record with regexpes applied for statistics
+                       my $rec = $ll_db->fetch_rec($pos);
+
                        foreach my $fld (keys %{ $rec }) {
                                $self->{_stats}->{fld}->{ $fld }++;
 
@@ -342,6 +353,7 @@ sub open {
                                        if (ref($row) eq 'HASH') {
 
                                                foreach my $sf (keys %{ $row }) {
+                                                       next if ($sf eq 'subfields');
                                                        $self->{_stats}->{sf}->{ $fld }->{ $sf }->{count}++;
                                                        $self->{_stats}->{sf}->{ $fld }->{ $sf }->{repeatable}++
                                                                        if (ref($row->{$sf}) eq 'ARRAY');
@@ -365,6 +377,9 @@ sub open {
        $self->{max_pos} = $to_rec;
        $log->debug("max_pos: $to_rec");
 
+       # save for dump
+       $self->{ll_db} = $ll_db;
+
        return $size;
 }
 
@@ -526,9 +541,22 @@ sub stats {
        return $out;
 }
 
+=head2 dump
+
+Display humanly readable dump of record
+
+=cut
+
+sub dump {
+       my $self = shift;
+
+       return $self->{ll_db}->dump_rec( $self->{pos} );
+
+}
+
 =head2 modify_record_regexps
 
-Generate hash with regexpes to be applied using L<filter>.
+Generate hash with regexpes to be applied using l<filter>.
 
   my $regexpes = $input->modify_record_regexps(
                900 => { '^a' => { ' : ' => '^b' } },
@@ -537,33 +565,39 @@ Generate hash with regexpes to be applied using L<filter>.
 
 =cut
 
+sub _get_regex {
+       my ($sf,$from,$to) = @_;
+       if ($sf =~ /^\^/) {
+               return
+                       's/\Q'. $sf .'\E([^\^]*?)\Q'. $from .'\E([^\^]*?)/'. $sf .'$1'. $to .'$2/';
+       } else {
+               return
+                       's/\Q'. $from .'\E/'. $to .'/g';
+       }
+}
+
 sub modify_record_regexps {
        my $self = shift;
        my $modify_record = {@_};
 
        my $regexpes;
 
+       my $log = $self->_get_logger();
+
        foreach my $f (keys %$modify_record) {
-warn "--- f: $f\n";
+               $log->debug("field: $f");
+
                foreach my $sf (keys %{ $modify_record->{$f} }) {
-warn "---- sf: $sf\n";
+                       $log->debug("subfield: $sf");
+
                        foreach my $from (keys %{ $modify_record->{$f}->{$sf} }) {
                                my $to = $modify_record->{$f}->{$sf}->{$from};
                                #die "no field?" unless defined($to);
-warn "----- transform: |$from| -> |$to|\n";
-
-                               if ($sf =~ /^\^/) {
-                                       my $regex = 
-                                               's/\Q'. $sf .'\E([^\^]+)\Q'. $from .'\E([^\^]+)/'. $sf .'$1'. $to .'$2/g';
-                                       push @{ $regexpes->{$f} }, $regex;
-warn ">>>>> $regex [sf]\n";
-                               } else {
-                                       my $regex =
-                                               's/\Q'. $from .'\E/'. $to .'/g';
-                                       push @{ $regexpes->{$f} }, $regex;
-warn ">>>>> $regex [global]\n";
-                               }
+                               $log->debug("transform: |$from| -> |$to|");
 
+                               my $regex = _get_regex($sf,$from,$to);
+                               push @{ $regexpes->{$f} }, $regex;
+                               $log->debug("regex: $regex");
                        }
                }
        }
@@ -571,6 +605,66 @@ warn ">>>>> $regex [global]\n";
        return $regexpes;
 }
 
+=head2 modify_file_regexps
+
+Generate hash with regexpes to be applied using l<filter> from
+pseudo hash/yaml format for regex mappings.
+
+It should be obvious:
+
+       200
+         '^a'
+           ' : ' => '^e'
+           ' = ' => '^d'
+
+In field I<200> find C<'^a'> and then C<' : '>, and replace it with C<'^e'>.
+In field I<200> find C<'^a'> and then C<' = '>, and replace it with C<'^d'>.
+
+  my $regexpes = $input->modify_file_regexps( 'conf/modify/common.pl' );
+
+On undef path it will just return.
+
+=cut
+
+sub modify_file_regexps {
+       my $self = shift;
+
+       my $modify_path = shift || return;
+
+       my $log = $self->_get_logger();
+
+       my $regexpes;
+
+       CORE::open(my $fh, $modify_path) || $log->logdie("can't open modify file $modify_path: $!");
+
+       my ($f,$sf);
+
+       while(<$fh>) {
+               chomp;
+               next if (/^#/ || /^\s*$/);
+
+               if (/^\s*(\d+)\s*$/) {
+                       $f = $1;
+                       $log->debug("field: $f");
+                       next;
+               } elsif (/^\s*'([^']*)'\s*$/) {
+                       $sf = $1;
+                       $log->die("can't define subfiled before field in: $_") unless ($f);
+                       $log->debug("subfield: $sf");
+               } elsif (/^\s*'([^']*)'\s*=>\s*'([^']*)'\s*$/) {
+                       my ($from,$to) = ($1, $2);
+
+                       $log->debug("transform: |$from| -> |$to|");
+
+                       my $regex = _get_regex($sf,$from,$to);
+                       push @{ $regexpes->{$f} }, $regex;
+                       $log->debug("regex: $regex");
+               }
+       }
+
+       return $regexpes;
+}
+
 =head1 MEMORY USAGE
 
 C<low_mem> options is double-edged sword. If enabled, WebPAC