added to_hash method and hash_filter coderef to new constructor to filter
[Biblio-Isis] / IsisDB.pm
index 13f173b..c5c6220 100644 (file)
--- a/IsisDB.pm
+++ b/IsisDB.pm
@@ -7,7 +7,7 @@ use Data::Dumper;
 BEGIN {
        use Exporter ();
        use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
-       $VERSION     = 0.02;
+       $VERSION     = 0.04;
        @ISA         = qw (Exporter);
        #Give a hoot don't pollute, do not export more than needed by default
        @EXPORT      = qw ();
@@ -22,16 +22,29 @@ IsisDB - Read CDS/ISIS database
 
 =head1 SYNOPSIS
 
-  use IsisDB
+  use IsisDB;
+
   my $isis = new IsisDB(
        isisdb => './cds/cds',
   );
 
+  for(my $mfn = 1; $mfn <= $isis->{'maxmfn'}; $mfn++) {
+       print $isis->to_ascii($mfn),"\n";
+  }
+
 =head1 DESCRIPTION
 
 This module will read CDS/ISIS databases and create hash values out of it.
 It can be used as perl-only alternative to OpenIsis module.
 
+This will module will always be slower that OpenIsis module which use C
+library. However, since it's written in perl, it's platform independent (so
+you don't need C compiler), and can be easily modified.
+
+Unique feature of this module is ability to C<include_deleted> records.
+It will also skip zero sized fields (OpenIsis has a bug in XS bindings, so
+fields which are zero sized will be filled with random junk from memory).
+
 =head1 METHODS
 
 =cut
@@ -50,14 +63,6 @@ It can be used as perl-only alternative to OpenIsis module.
 # some binary reads
 #
 
-sub Read32 {
-       my $self = shift;
-
-       my $f = shift || die "Read32 needs file handle";
-       read($$f,$b,4) || die "can't read 4 bytes from $$f from position ".tell($f);
-       return unpack("l",$b);
-}
-
 =head2 new
 
 Open CDS/ISIS database
@@ -65,6 +70,11 @@ Open CDS/ISIS database
  my $isis = new IsisDB(
        isisdb => './cds/cds',
        read_fdt => 1,
+       include_deleted => 1,
+       hash_filter => sub {
+               my $v = shift;
+               $v =~ s#foo#bar#g;
+       },
        debug => 1,
  );
 
@@ -82,9 +92,17 @@ and common prefix of C<.FDT>, C<.MST>, C<.CNT>, C<.XRF> and C<.MST> files.
 Boolean flag to specify if field definition table should be read. It's off
 by default.
 
+=item include_deleted
+
+Don't skip logically deleted records in ISIS.
+
+=item hash_filter
+
+Filter code ref which will be used before data is converted to hash.
+
 =item debug
 
-Dump a C<lot> of debugging output.
+Dump a B<lot> of debugging output.
 
 =back
 
@@ -97,9 +115,11 @@ sub new {
        my $self = {};
        bless($self, $class);
 
-       $self->{isisdb} = {@_}->{isisdb} || croak "new needs database name as argument!";
+       croak "new needs database name (isisdb) as argument!" unless ({@_}->{isisdb});
 
-       $self->{debug} = {@_}->{debug};
+       foreach my $v (qw{isisdb debug include_deleted hash_filter}) {
+               $self->{$v} = {@_}->{$v};
+       }
 
        # if you want to read .FDT file use read_fdt argument when creating class!
        if ({@_}->{read_fdt} && -e $self->{isisdb}.".FDT") {
@@ -140,7 +160,11 @@ sub new {
        # NXTMFP        offset to next available position in last block
        # MFTYPE        always 0 for user db file (1 for system)
        seek(fileMST,4,0);
-       $self->{'NXTMFN'}=$self->Read32(\*fileMST) || carp "NXTNFN is zero";
+
+       my $buff;
+
+       read(fileMST, $buff, 4);
+       $self->{'NXTMFN'}=unpack("l",$buff) || carp "NXTNFN is zero";
 
        # save maximum MFN
        $self->{'maxmfn'} = $self->{'NXTMFN'} - 1;
@@ -173,13 +197,14 @@ sub new {
                my $buff = shift || return;
                my @arr = unpack("ssssssllls", $buff);
 
+               print "unpack_cnt: ",join(" ",@arr),"\n" if ($self->{'debug'});
+
                my $IDTYPE = shift @arr;
                foreach (@flds) {
                        $self->{$IDTYPE}->{$_} = abs(shift @arr);
                }
        }
 
-       my $buff;
        read(fileCNT, $buff, 26);
        $self->unpack_cnt($buff);
 
@@ -206,7 +231,7 @@ Read record with selected MFN
   my $rec = $isis->fetch(55);
 
 Returns hash with keys which are field names and values are unpacked values
-for that field.
+for that field (like C<^asometing^bsomething else>)
 
 =cut
 
@@ -223,8 +248,11 @@ sub fetch {
        print "seeking to $mfnpos in file '$self->{isisdb}.XRF'\n" if ($self->{debug});
        seek($self->{'fileXRF'},$mfnpos,0);
 
+       my $buff;
+
        # read XRFMFB abd XRFMFP
-       my $pointer=$self->Read32(\*{$self->{'fileXRF'}});
+       read($self->{'fileXRF'}, $buff, 4);
+       my $pointer=unpack("l",$buff) || carp "pointer is null";
 
        my $XRFMFB = int($pointer/2048);
        my $XRFMFP = $pointer - ($XRFMFB*2048);
@@ -249,7 +277,8 @@ sub fetch {
 
        seek($self->{'fileMST'},$offset4,0);
 
-       my $value=$self->Read32(\*{$self->{'fileMST'}});
+       read($self->{'fileMST'}, $buff, 4);
+       my $value=unpack("l",$buff);
 
        if ($value!=$mfn) {
 print ("Error: The MFN:".$mfn." is not found in MST(".$value.")");    
@@ -263,27 +292,37 @@ print ("Error: The MFN:".$mfn." is not found in MST(".$value.")");
 #      $NVF=$self->Read16($fileMST);
 #      $STATUS=$self->Read16($fileMST);
 
-       my $buff;
        read($self->{'fileMST'}, $buff, 14);
 
        my ($MFRL,$MFBWB,$MFBWP,$BASE,$NVF,$STATUS) = unpack("slssss", $buff);
 
        print "MFRL: $MFRL MFBWB: $MFBWB MFBWP: $MFBWP BASE: $BASE NVF: $NVF STATUS: $STATUS\n" if ($self->{debug});
 
+       # delete old record
+       delete $self->{record};
+
+       if (! $self->{'include_deleted'} && $MFRL < 0) {
+               print "## logically deleted record $mfn, skipping...\n" if ($self->{debug});
+               return;
+       }
+
        # Get Directory Format
 
        my @FieldPOS;
        my @FieldLEN;
        my @FieldTAG;
 
+       read($self->{'fileMST'}, $buff, 6 * $NVF);
+
+       my $fld_len = 0;
+
        for (my $i = 0 ; $i < $NVF ; $i++) {
 
 #              $TAG=$self->Read16($fileMST);
 #              $POS=$self->Read16($fileMST);
 #              $LEN=$self->Read16($fileMST);
 
-               read($self->{'fileMST'}, $buff, 6);
-               my ($TAG,$POS,$LEN) = unpack("sss", $buff);
+               my ($TAG,$POS,$LEN) = unpack("sss", substr($buff,$i * 6, 6));
 
                print "TAG: $TAG POS: $POS LEN: $LEN\n" if ($self->{debug});
 
@@ -300,23 +339,21 @@ print ("Error: The MFN:".$mfn." is not found in MST(".$value.")");
                push @FieldTAG,$TAG;
                push @FieldPOS,$POS;
                push @FieldLEN,$LEN;
+
+               $fld_len += $LEN;
        }
 
        # Get Variable Fields
 
-       delete $self->{record};
+       read($self->{'fileMST'},$buff,$fld_len);
 
        for (my $i = 0 ; $i < $NVF ; $i++) {
-               my $rec;
-               read($self->{'fileMST'},$rec,$FieldLEN[$i]);
-               push @{$self->{record}->{$FieldTAG[$i]}}, $rec;
-       }
-       close(fileMST);
+               # skip zero-sized fields
+               next if ($FieldLEN[$i] == 0);
 
-       # The record is marked for deletion
-       if ($STATUS==1) {
-               return -1;
+               push @{$self->{record}->{$FieldTAG[$i]}}, substr($buff,$FieldPOS[$i],$FieldLEN[$i]);
        }
+       close(fileMST);
 
        print Dumper($self) if ($self->{debug});
 
@@ -349,6 +386,69 @@ sub to_ascii {
        return $out;
 }
 
+=head2 to_hash
+
+Read mfn and convert it to hash
+
+  my $hash = $isis->to_hash($mfn);
+
+It has ability to convert characters (using C<hash_filter> from ISIS
+database before creating structures enabling character remapping or quick
+fixup of data.
+
+This function returns hash which is like this:
+
+  $hash = {
+    '210' => [
+               {
+                 'c' => 'New York University press',
+                 'a' => 'New York',
+                 'd' => 'cop. 1988'
+               }
+             ],
+    '990' => [
+               '2140',
+               '88',
+               'HAY'
+             ],
+  };
+
+You can later use that has to produce any output from ISIS data.
+
+=cut
+
+sub to_hash {
+       my $self = shift;
+
+       my $mfn = shift || confess "need mfn!";
+
+       my $rec;
+       my $row = $self->fetch($mfn);
+
+       foreach my $k (keys %{$row}) {
+               foreach my $l (@{$row->{$k}}) {
+
+                       # filter output
+                       $l = $self->{'hash_filter'}->($l) if ($self->{'hash_filter'});
+
+                       # has subfields?
+                       my $val;
+                       if ($l =~ m/\^/) {
+                               foreach my $t (split(/\^/,$l)) {
+                                       next if (! $t);
+                                       $val->{substr($t,0,1)} = substr($t,1);
+                               }
+                       } else {
+                               $val = $l;
+                       }
+
+                       push @{$rec->{$k}}, $val;
+               }
+       }
+
+       return $rec;
+}
+
 #
 # XXX porting from php left-over:
 #