/[SWISH-PlusPlus]/trunk/PlusPlus.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/PlusPlus.pm

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

revision 16 by dpavlin, Sun Dec 5 21:06:48 2004 UTC revision 22 by dpavlin, Tue Dec 7 16:05:43 2004 UTC
# Line 4  use 5.008004; Line 4  use 5.008004;
4  use strict;  use strict;
5  use warnings;  use warnings;
6    
7  our $VERSION = '0.10';  our $VERSION = '0.20';
8    
9  use Carp;  use Carp;
10  use File::Temp qw/ tempdir /;  use File::Temp qw/ tempdir /;
11  use BerkeleyDB;  use BerkeleyDB;
12  #use YAML;  use Storable qw(store retrieve freeze thaw);
13    use YAML;
14    
15  =head1 NAME  =head1 NAME
16    
17  SWISH::PlusPlus - Perl extension SWISH++  SWISH::PlusPlus - Perl extension for full-text indexer SWISH++ with properties support
18    
19  =head1 SYNOPSIS  =head1 SYNOPSIS
20    
21    use SWISH::PlusPlus;    use SWISH::PlusPlus;
22    blah blah blah  
23      my $i = new SWISH::PlusPlus(
24            index_dir => '/tmp/foo',
25      );
26      $i->add( 42 => 'meaning of life' );
27    
28      print $i->search("meaning");  # returns 42
29    
30  =head1 DESCRIPTION  =head1 DESCRIPTION
31    
32  This is perl module to use SWISH++ indexer by Paul J. Lucas. SWISH++ is  This is perl module to use SWISH++ indexer by Paul J. Lucas. SWISH++ is
33  rewrite of swish-e in C++ which is extremly fast (thank to mmap), but without  rewrite of swish-e in C++ which is extremely fast (due to mmap usage and
34  support for properties (which this module tries to fix).  clever language heuristics), but without support for properties (which this
35    module tries to fix).
36  Implementation of this module is crafted after L<Plucene::Simple> and it  
37  should be easy to replace Plucene with this module for increased  Implementation of API is something in-between C<SWISH::API> and
38  performance. However, this module is not plug-in replacement.  C<Plucene::Simple>. It should be easy to replace Plucene or swish-e with
39    this module for increased performance. However, this module is not plug-in
40    replacement.
41    
42  =head1 METHODS  =head1 METHODS
43    
44  =head2 new  =head2 new
45    
46  Create new indexing object.  Create new instance for index.
47    
48    my $i = SWISH::PlusPlus->new(    my $i = SWISH::PlusPlus->new(
49          index_dir => '/path/to/index',          index_dir => '/path/to/index',
# Line 45  Create new indexing object. Line 54  Create new indexing object.
54          use_stopwords => 1,          use_stopwords => 1,
55    );    );
56    
57  Options to new are following:  Options are described below:
58    
59  =over 5  =over 5
60    
61  =item C<index_dir>  =item C<index_dir>
62    
63  Path to directory in which index will be created.  Path to directory in which index and meta database will be created.
64    
65  =item C<index>  =item C<index>
66    
# Line 71  C<STDERR> prefixed by C<##>. Line 80  C<STDERR> prefixed by C<##>.
80  =item C<meta_in_body>  =item C<meta_in_body>
81    
82  This option (off by default) enables to search content of meta fields  This option (off by default) enables to search content of meta fields
83  without specifing them (like they are in body of document). This will  without specifying them (like they are in body of document). This will
84  somewhat increate index size.  somewhat increase index size.
85    
86  =item C<use_stopwords>  =item C<use_stopwords>
87    
# Line 119  sub new { Line 128  sub new {
128    
129  =head2 check_bin  =head2 check_bin
130    
131  Check if swish++ binaries specified in L<new> are available and verify  Check if SWISH++ binaries specified in L<new> are available and verify
132  version signature.  version signature.
133    
134    if ($i->check_bin) {    if ($i->check_bin) {
# Line 130  It will also setup property Line 139  It will also setup property
139    
140    $i->{'version'}    $i->{'version'}
141    
142  which you can examine to see version.  which you can examined to see numeric version (something like C<6.0.4>).
143    
144  =cut  =cut
145    
# Line 161  sub check_bin { Line 170  sub check_bin {
170    
171  Quick way to add simple data to index.  Quick way to add simple data to index.
172    
173    $i->index_document($key, $data);    $i->index_document($path, $data);
174    $i->index_document( 42 => 'meaning of life' );    $i->index_document(
175            42 => 'meaning of life',
176            1984 => 'Oh!',
177      );
178    
179    C<$path> value is really path, so you don't want to use directory
180    separators (slashes, /) in it probably.
181    
182  =cut  =cut
183    
# Line 183  sub index_document { Line 198  sub index_document {
198    
199  =head2 add  =head2 add
200    
201  Add document with metadata to index.  Add document with meta-data to index.
202    
203    $i->add(    $i->add(
204          path => 'path/to/document',          path => 'path/to/document',
# Line 203  This is thin wrapper round L<_create_doc Line 218  This is thin wrapper round L<_create_doc
218  sub add {  sub add {
219          my $self = shift;          my $self = shift;
220    
221          $self->_create_doc(@_);          return $self->_create_doc(@_);
222    }
223    
224          return 1;  
225    =head2 delete
226    
227    Delete document from index.
228    
229      $i->delete("document/path");
230    
231    If deletion is succesfull returns revision of deleted document, otherwise
232    undef.
233    
234    =cut
235    
236    sub delete {
237            my $self = shift;
238    
239            my $path = shift || carp "empty path?";
240    
241            print STDERR "## delete: $path\n" if ($self->{'debug'});
242    
243            my $rev = $self->{'meta_db'}->{"R$path"};
244            if ($rev) {
245                    $self->{'_deleted'}->{$path} = $rev;
246                    $self->{'_deleted_counter'}++;
247                    print STDERR "## deleted revision $rev, counter: ",$self->{'_deleted_counter'}++,"\n" if ($self->{'debug'});
248                    return $rev;
249            }
250    
251            return undef;
252  }  }
253    
254    
255  =head2 search  =head2 search
256    
257  Search your index.  Search your index using any valid SWISH++ query.
258    
259    my @results = $i->search("swhish query");    my @results = $i->search("swish query");
260    
261  Returns array with result IDs.  Returns array with elements like this:
262    
263      {
264       rank => 10,                  # rank of result
265       path => 'path to result',    # path to result
266       size => 999,                 # size in bytes
267       title => 'title of result'   # title meta property
268      }
269    
270  =cut  =cut
271    
# Line 236  sub search { Line 288  sub search {
288                  ' |';                  ' |';
289          print STDERR "## search: $open_cmd\n" if ($self->{'debug'});          print STDERR "## search: $open_cmd\n" if ($self->{'debug'});
290    
291            my %r;
292    
293          open(SEARCH, $open_cmd) || confess "can't start $open_cmd: $!";          open(SEARCH, $open_cmd) || confess "can't start $open_cmd: $!";
294          my $l;          my $l;
295          while($l = <SEARCH>) {          while($l = <SEARCH>) {
296                  next if ($l =~ /^#/);                  next if ($l =~ /^#/);
297                  chomp($l);                  chomp($l);
298                  print STDERR "## $l\n" if ($self->{'debug'});                  print STDERR "## $l\n" if ($self->{'debug'});
299                  my ($rank,$path,$size,$title) = split(/ /,$l,4);                  my ($rank,$path,$size,$rev,$title) = split(/ /,$l,5);
300                  $path =~ s#^\./##; # strip from path                  $path =~ s#^\./##; # strip from path
301    
302                    # get current revision
303                    $r{$path} = $self->{'meta_db'}->{"R$path"};
304    
305                    # skip if old revision
306                    next if ($r{$path} > $rev);
307    
308                    print STDERR "## current revision $rev\n" if ($self->{'debug'});
309    
310                  push @results, {                  push @results, {
311                          rank => $rank,                          rank => $rank,
312                          path => $path,                          path => $path,
313                          size => $size,                          size => $size,
314                          title => $title,                          title => $title,
315                  }                  } unless ($self->{'_deleted'}->{$path} && $self->{'_deleted'}->{$path} <= $rev);
316          }          }
317    
318          close(SEARCH) || confess "can't close search";          close(SEARCH) || confess "can't close search";
# Line 263  sub search { Line 326  sub search {
326    
327  Return stored meta property from result or result path.  Return stored meta property from result or result path.
328    
329    print $i->property('path', 'title');    print $i->property('path', 'meta name');
330    print $i->property($res->{'path'}, 'title');    print $i->property($res->{'path'}, 'meta name');
331      print $i->property('path');
332      print $i->property($res->{'path'});
333    
334    Returns one meta property (if meta name is specified) or whole hash with
335    all meta properties.
336    
337  =cut  =cut
338    
339  sub property {  sub property {
340          my $self = shift;          my $self = shift;
341    
342          my ($path,$meta) = @_;          my $path = shift || return;
343            my $meta = shift;
344    
345          if ($path =~ m/^HASH/) {          if ($path =~ m/^HASH/) {
346                  $path = $path->{'path'} || confess "can't find path in input data";                  $path = $path->{'path'} || confess "can't find path in input data";
347          }          }
348    
349          my $val = $self->{'meta_db'}->{"$path-$meta"};          my $val = $self->{'meta_db'}->{"M$path"};
350    
351            # FIXME should we die here like swish-e does?
352            return unless ($val);
353    
354            $val = thaw($val);
355    
356            print STDERR "## property $path $meta: ",(Dump($val) || 'undef'),"\n" if ($self->{'debug'});
357    
358            return $val->{$meta} if ($meta);
359    
         print STDERR "## property $path-$meta: ",($val || 'undef'),"\n" if ($self->{'debug'});  
360          return $val;          return $val;
361  }  }
362    
363  =head2 finish_update  =head2 finish_update
364    
365  This method will close index.  This method will close index binary and enable search. Searching is not
366    available while indexing is in process.
367    
368    $i->finish_update;    $i->finish_update;
369    
370  It will be called on DESTROY when $i goes out of scope.  Usually, you don't need to call this method directly. It will be called on
371    DESTROY when $i goes out of scope or when you first call search in session
372    if indexing was started.
373    
374  =cut  =cut
375    
# Line 308  sub DESTROY { Line 388  sub DESTROY {
388    
389  =head1 PRIVATE METHODS  =head1 PRIVATE METHODS
390    
391  Private methods implement internals for creating temporary file needed for  Private methods implement internals for creating temporary files needed for
392  swish++. You should have no need to call them directly, and they are here  SWISH++. You should have no need to call them directly, and they are here
393  just to have documentation.  just to have documentation.
394    
395  =head2 _init_indexer  =head2 _init_indexer
# Line 333  sub _init_indexer { Line 413  sub _init_indexer {
413    
414          chdir $tmp_dir || confess "can't chdir to ".$tmp_dir.": $!";          chdir $tmp_dir || confess "can't chdir to ".$tmp_dir.": $!";
415    
416          print STDERR "## tmp_dir: $tmp_dir" if ($self->{'debug'});          print STDERR "## tmp_dir: $tmp_dir\n" if ($self->{'debug'});
417    
418          my $opt = "-v " . ($self->{'debug'} || '0');          my $opt = "-v " . ($self->{'debug'} || '0');
419    
420            my $index_dir = $self->{'index_dir'} || confess "no index_dir?";
421            my $index_file = $index_dir . '/index';
422    
423            if (-e $index_file && ! -z $index_file) {
424                    $opt .= ' -I ';
425                    $self->{'_incremental'} = 1;
426                    print STDERR "## using incremental indexing for $index_file\n" if ($self->{'debug'});
427            } else {
428                    $self->{'_incremental'} = 0;
429            }
430    
431          unless ($self->{'use_stopwrods'}) {          unless ($self->{'use_stopwrods'}) {
432                  open(STOP, '>', "_stopwords_") || carp "can't create empty stopword file, skipping\n";                  open(STOP, '>', "_stopwords_") || carp "can't create empty stopword file, skipping\n";
433                  print STOP "  ";                  print STOP "  ";
# Line 344  sub _init_indexer { Line 435  sub _init_indexer {
435                  $opt .= " -s _stopwords_";                  $opt .= " -s _stopwords_";
436          }          }
437    
438          my $index_dir = $self->{'index_dir'} || confess "no index_dir?";          my $open_cmd = '| '.$self->{'index'}.' '.$opt.' -e "html:*" -i '.$index_file.' -';
   
         my $open_cmd = '| '.$self->{'index'}.' '.$opt.' -e "html:*" -i '.$index_dir.'/index -';  
439    
440          print STDERR "## init_indexer: $open_cmd\n" if ($self->{'debug'});          print STDERR "## init_indexer: $open_cmd\n" if ($self->{'debug'});
441    
# Line 359  sub _init_indexer { Line 448  sub _init_indexer {
448          return $self->{'_index_fh'};          return $self->{'_index_fh'};
449  }  }
450    
 =head2 _tie_meta_db  
   
 Open BerkeleyDB database with meta properties.  
   
   $i->_tie_meta_db(DB_CREATE);  
   $i->_tie_meta_db(DB_RDONLY);  
   
 }  
   
 =cut  
   
 sub _tie_meta_db  {  
         my $self = shift;  
   
         my $flags = shift || confess "need DB_CREATE or DB_RDONLY";  
   
         return if ($self->{'_meta_db_flags'} && $self->{'_meta_db_flags'} == $flags);  
   
         print STDERR "## _tie_meta_db($flags)\n" if ($self->{'debug'});  
   
         $self->_untie_meta_db;  
         $self->{'_meta_db_flags'} = $flags;  
   
         my $file = $self->{'index_dir'}.'/meta.db';  
   
         tie %{$self->{'meta_db'}}, "BerkeleyDB::Hash",  
                 -Filename => $file,  
                 -Flags    => $flags  
         or confess "cannot open $file: $! $BerkeleyDB::Error\n" ;  
   
         return 1;  
 }  
   
 =head2 _untie_meta_db  
   
 Close BerkeleyDB database with meta properties.  
   
   $i->_untie_meta_db  
   
 =cut  
   
 sub _untie_meta_db {  
         my $self = shift;  
   
         return unless ($self->{'meta_db'});  
   
         print STDERR "## _untie_meta_db\n" if ($self->{'debug'});  
         untie %{$self->{'meta_db'}} || confess "can't untie!";  
         undef $self->{'meta_db'};  
         undef $self->{'_meta_db_flags'};  
   
         return 1;  
 }  
   
451  =head2 _create_doc  =head2 _create_doc
452    
453  Create temporary file and pass it's name to swish++  Create temporary file and pass it's name to SWISH++
454    
455    $i->_create_doc(    $i->_create_doc(
456          path => 'path/to/store/in/index',          path => 'path/to/store/in/index',
# Line 427  Create temporary file and pass it's name Line 462  Create temporary file and pass it's name
462          }          }
463    );    );
464    
 To delete document, just omit body and meta data.  
   
465  =cut  =cut
466    
467  sub _create_doc {  sub _create_doc {
# Line 443  sub _create_doc { Line 476  sub _create_doc {
476          my $id = $arg->{'path'} || confess "no path?";          my $id = $arg->{'path'} || confess "no path?";
477          $path .= "/$id";          $path .= "/$id";
478    
479          print STDERR "## _create_doc: $path\n" if ($self->{'debug'});          my $rev = $self->{'rev'}++;
480    
481            print STDERR "## _create_doc: $path [$rev]\n" if ($self->{'debug'});
482    
483          open(TMP, '>', $path) || die "can't create temp file $path: $!";          open(TMP, '>', $path) || die "can't create temp file $path: $!";
484    
485          print TMP '<html><head>';          print TMP '<html><head>';
486    
487          $arg->{'body'} ||= '';          my $body = $arg->{'body'};
488    
489            if (defined($body)) {
490                    $self->{'meta_db'}->{"B$id"} = $body;
491            } else {
492                    $body = '';
493            }
494    
495            my $title = $arg->{'title'};
496    
497          if ($arg->{'meta'}) {          if ($arg->{'meta'}) {
498                  foreach my $name (keys %{$arg->{'meta'}}) {                  foreach my $name (keys %{$arg->{'meta'}}) {
499                          my $content = $arg->{'meta'}->{$name};                          my $content = $arg->{'meta'}->{$name};
500                          print TMP qq{<meta name="$name" content="$content">};                          print TMP qq{<meta name="$name" content="$content">};
501                          $arg->{'body'} .= " $content" if ($self->{'meta_in_body'});                          $body .= " $content" if ($self->{'meta_in_body'});
                         $self->{'meta_db'}->{"$id-$name"} = $content;  
502                  }                  }
503                    $arg->{'meta'}->{'title'} = $title;
504                    $self->{'meta_db'}->{"M$id"} = freeze($arg->{'meta'});
505          }          }
506    
         my $title = $arg->{'title'};  
507          if (defined($title)) {          if (defined($title)) {
508                  print TMP "<title>$title</title>";                  $title = "$rev $title";
509                  $arg->{'body'} .= " $title" if ($self->{'meta_in_body'});                  $body .= " $title" if ($self->{'meta_in_body'});
510                  $self->{'meta_db'}->{"$id-title"} = $title;          } else {
511                    $title = "$rev $id";
512          }          }
513    
514          print TMP '</head><body>' . $arg->{'body'} . '</body></html>';          # dump html
515            print TMP "<title>$title</title></head><body>$body</body></html>";
516                    
517          close(TMP) || confess "can't close tmp file ".$arg->{'path'}.": $!";          close(TMP) || confess "can't close tmp file ".$arg->{'path'}.": $!";
518    
519          print { $self->{'_index_fh'} } "$id\n";          print { $self->{'_index_fh'} } "$id\n" || confess "can't pass document $id to indexer: $!";
520            
521            $self->{'meta_db'}->{"R$id"} = $rev;
522    
523            # FIXME this is probably not the right place to update global
524            # maximum revision, but it keeps database in sane state
525            $self->{'meta_db'}->{"Crev"} = $rev;
526  }  }
527    
528  =head2 _close_index  =head2 _close_index
# Line 487  You have to close index before searching Line 538  You have to close index before searching
538  sub _close_index {  sub _close_index {
539          my $self = shift;          my $self = shift;
540    
541            $self->_store_deleted;
542    
543          return unless ($self->{'_index_fh'});          return unless ($self->{'_index_fh'});
544    
545          print STDERR "## close index\n" if ($self->{'debug'});          print STDERR "## close index\n" if ($self->{'debug'});
# Line 494  sub _close_index { Line 547  sub _close_index {
547          close($self->{'_index_fh'}) || confess "can't close index: $!";          close($self->{'_index_fh'}) || confess "can't close index: $!";
548          undef $self->{'_index_fh'};          undef $self->{'_index_fh'};
549    
550            if ($self->{'_incremental'}) {
551                    print STDERR "## move new index over old\n" if ($self->{'debug'});
552                    rename $self->{'index_dir'}.'/index.new',$self->{'index_dir'}.'/index' || die "can't move new index over old one: $!";
553            }
554    
555          return 1;          return 1;
556  }  }
557    
558    =head2 _tie_meta_db
559    
560    Open BerkeleyDB database with meta properties.
561    
562      $i->_tie_meta_db(DB_CREATE);
563      $i->_tie_meta_db(DB_RDONLY);
564    
565    }
566    
567    =cut
568    
569    sub _tie_meta_db  {
570            my $self = shift;
571    
572            my $flags = shift || confess "need DB_CREATE or DB_RDONLY";
573    
574            return if ($self->{'_meta_db_flags'} && $self->{'_meta_db_flags'} == $flags);
575    
576            print STDERR "## _tie_meta_db($flags)\n" if ($self->{'debug'});
577    
578            $self->_untie_meta_db;
579            $self->{'_meta_db_flags'} = $flags;
580    
581            my $file = $self->{'index_dir'}.'/meta.db';
582    
583            tie %{$self->{'meta_db'}}, "BerkeleyDB::Hash",
584                    -Filename => $file,
585                    -Flags    => $flags
586            or confess "cannot open $file: $! $BerkeleyDB::Error\n" ;
587    
588            $self->{'rev'} = $self->{'meta_db'}->{'Crev'} || 0;
589    
590            my $delref = $self->{'meta_db'}->{'Cdeleted'};
591            if ($delref) {
592                    $self->{'_deleted'} = thaw($delref);
593    
594                    print "## deleted ",keys %{$self->{'_deleted'}}," records\n" if ($self->{'debug'});
595            } else {
596                    $self->{'_deleted'} = {};
597            }
598    
599            $self->{'_deleted_counter'} = 0;
600            return 1;
601    }
602    
603    =head2 _untie_meta_db
604    
605    Close BerkeleyDB database with meta properties.
606    
607      $i->_untie_meta_db;
608    
609    =cut
610    
611    sub _untie_meta_db {
612            my $self = shift;
613    
614            return unless ($self->{'meta_db'});
615    
616            print STDERR "## _untie_meta_db\n" if ($self->{'debug'});
617            untie %{$self->{'meta_db'}} || confess "can't untie!";
618            undef $self->{'meta_db'};
619            undef $self->{'_meta_db_flags'};
620    
621            return 1;
622    }
623    
624    
625    =head2 _store_deleted
626    
627    Save hash of deleted files using L<Storable>.
628    
629      $i->_store_deleted;
630    
631    =cut
632    
633    sub _store_deleted {
634            my $self = shift;
635    
636            return if (! $self->{'_deleted_counter'});
637    
638            print STDERR "## save deleted ",Dump($self->{'_deleted'}) if ($self->{'debug'});
639    
640            my $d = freeze($self->{'_deleted'});
641    
642            $self->_tie_meta_db(DB_CREATE);
643    
644            $self->{'meta_db'}->{'Cdeleted'} = $d ||
645                    carp "can't store deleted: $!";
646    
647            # reset counter
648            $self->{'_deleted_counter'} = 0;
649    }
650    
651  1;  1;
652  __END__  __END__
653    
# Line 508  None by default. Line 659  None by default.
659    
660  =head2 Debian  =head2 Debian
661    
662  Debian version of swish++ is often old (version 5 at moment of this writing  Debian version of SWISH++ is often old (version 5 at moment of this writing
663  while version 6 is available in source code), so this module by default  while version 6 is available in source code), so this module by default
664  uses executable names B<index> and B<search> for self-compiled version  uses executable names B<index> and B<search> for self-compiled version
665  instead of one from Debian package. See L<new> how to specify Debian  instead of one from Debian package. See L<new> how to specify Debian
# Line 516  default binaries B<index++> and B<search Line 667  default binaries B<index++> and B<search
667    
668  =head2 SWISH++  =head2 SWISH++
669    
670  Aside from very good rewrite in C++, SWISH++ is fatster because it has  Aside from very good rewrite in C++, SWISH++ is faster because it uses
671  claver heuristics about which data in input files are words to index and  claver heuristics about which data in input files are words to index and
672  which are not. It's based on English language and might be best choice if  which are not. It's based on English language and might be best choice if
673  you plan to install large amount of long text documents.  you plan to index large amount of long text documents.
674    
675  However, if you plan to index all data from structured storage (e.g. RDBMS)  However, if you plan to index all data from structured storage (e.g. RDBMS)
676  you might want B<all> words from data to end up in index as opposed to just  you might want B<all> words from data to end up in index as opposed to just
# Line 527  those which look like English words. Thi Line 678  those which look like English words. Thi
678  don't plan to index English texts with this module.  don't plan to index English texts with this module.
679    
680  With distribution build versions of SWISH++ you might have problems with  With distribution build versions of SWISH++ you might have problems with
681  disepearing words. To overcome this problem, you will have to compile and  disapearing words. To overcome this problem, you will have to compile and
682  configure SWISH++ yourself (because language characteristics are  configure SWISH++ yourself (because language characteristics are
683  compilation-time option).  compilation-time option).
684    
# Line 543  configuration is needed for B<date test> Line 694  configuration is needed for B<date test>
694  doesn't recognize 2004-12-05 as date. Have in mind that your index size  doesn't recognize 2004-12-05 as date. Have in mind that your index size
695  might explode.  might explode.
696    
697    =head1 BUGS
698    
699    Currently there is no way to specify which meta data will be stored as
700    properties. B<This will be fixed very soon>.
701    
702    There is no garbage collection on temporary files created for SWISH++. This
703    means that one run of indexer will take additional disk space for temporary
704    files, which will be removed at end. There should be some way to remove
705    files after they are indexed by SWISH++. However, at this early stage of
706    development it's just not supported yet. Have plenty of disk space!
707    
708  =head1 SEE ALSO  =head1 SEE ALSO
709    
710  C<swish++> web site L<http://homepage.mac.com/pauljlucas/software/swish/>  SWISH++ web site L<http://homepage.mac.com/pauljlucas/software/swish/>
711    
712  =head1 AUTHOR  =head1 AUTHOR
713    

Legend:
Removed from v.16  
changed lines
  Added in v.22

  ViewVC Help
Powered by ViewVC 1.1.26