Support, testing and changelog for multi-word terms quoted by {curly brackets}
[perl-pqf.git] / lib / Net / Z3950 / PQF.pm
index 92aae90..3c78a7f 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: PQF.pm,v 1.2 2004-12-17 15:28:30 mike Exp $
+# $Id: PQF.pm,v 1.7 2004-12-23 10:24:12 mike Exp $
 
 package Net::Z3950::PQF;
 
 
 package Net::Z3950::PQF;
 
@@ -8,7 +8,7 @@ use warnings;
 
 use Net::Z3950::PQF::Node;
 
 
 use Net::Z3950::PQF::Node;
 
-our $VERSION = '0.02';
+our $VERSION = '0.03';
 
 
 =head1 NAME
 
 
 =head1 NAME
@@ -28,9 +28,11 @@ This library provides a parser for PQF (Prefix Query Format), an ugly
 but precise string format for expressing Z39.50 Type-1 queries.  This
 format is widely used behind the scenes of Z39.50 applications, and is
 also used extensively with test-harness programs such as the YAZ
 but precise string format for expressing Z39.50 Type-1 queries.  This
 format is widely used behind the scenes of Z39.50 applications, and is
 also used extensively with test-harness programs such as the YAZ
-command-line client, C<yaz-client>.
+command-line client, C<yaz-client>.  A few particularly misguided
+souls have been known to type it by hand.
 
 
-It is simple to use.  Create a parser object, then pass PQF strings
+Unlike PQF itself, this module
+is simple to use.  Create a parser object, then pass PQF strings
 into its C<parse()> method to yield parse-trees.  The trees are made
 up of nodes whose types are subclasses of
 C<Net::Z3950::PQF::Node>.
 into its C<parse()> method to yield parse-trees.  The trees are made
 up of nodes whose types are subclasses of
 C<Net::Z3950::PQF::Node>.
@@ -59,6 +61,7 @@ sub new {
     my $class = shift();
 
     return bless {
     my $class = shift();
 
     return bless {
+       text => undef,
        errmsg => undef,
     }, $class;
 }
        errmsg => undef,
     }, $class;
 }
@@ -68,9 +71,9 @@ sub new {
 
  $query = '@and @attr 1=1003 kernighan @attr 1=4 unix';
  $node = $parser->parse($query);
 
  $query = '@and @attr 1=1003 kernighan @attr 1=4 unix';
  $node = $parser->parse($query);
- if (!defined $node)
+ if (!defined $node) {
      die "parse($query) failed: " . $parser->errmsg();
      die "parse($query) failed: " . $parser->errmsg();
- } 
+ }
 
 Parses the PQF string provided as its argument.  If an error occurs,
 then an undefined value is returned, and the error message can be
 
 Parses the PQF string provided as its argument.  If an error occurs,
 then an undefined value is returned, and the error message can be
@@ -80,20 +83,131 @@ of the parse tree is returned.
  $node2 = $parser->parse($query, "zthes");
  $node3 = $parser->parse($query, "1.2.840.10003.3.13");
 
  $node2 = $parser->parse($query, "zthes");
  $node3 = $parser->parse($query, "1.2.840.10003.3.13");
 
-A second argument may be provided, after the query itself.  If it is
+A second argument may be provided after the query itself.  If it is
 provided, then it is taken to be either the name or the OID of a
 default attribute set, which attributes specified in the query belong
 provided, then it is taken to be either the name or the OID of a
 default attribute set, which attributes specified in the query belong
-to if no alternative attribute set is explicitly specified.  When this
-second argument is absent, the default attribute set is BIB-1.
+to if no alternative attribute set is explicitly specified within the
+query.  When this second argument is absent, the default attribute set
+is BIB-1.
 
 =cut
 
 sub parse {
     my $this = shift();
 
 =cut
 
 sub parse {
     my $this = shift();
-    my($attrset) = @_;
+    my($text, $attrset) = @_;
     $attrset = "bib-1" if !defined $attrset;
 
     $attrset = "bib-1" if !defined $attrset;
 
-    die "parse($this) not yet implemented";
+    $this->{text} = $text;
+    return $this->_parse($attrset, {});
+}
+
+
+# PRIVATE to parse();
+#
+# Underlying parse function.  $attrset is the default attribute-set to
+# use for attributes that are not specified with an explicit set, and
+# $attrhash is hash of attributes (at most one per type per
+# attribute-set) to be applied to all nodes below this point.  The
+# keys of this hash are of the form "<attrset>:<type>" and the values
+# are the corresponding attribute values.
+#
+sub _parse {
+    my $this = shift();
+    my($attrset, $attrhash) = @_;
+
+    $this->{text} =~ s/^\s+//;
+
+    ###        This rather nasty hack for quoted terms doesn't recognised
+    #  backslash-quoted embedded double quotes.
+    if ($this->{text} =~ s/^"(.*?)"//) {
+       return $this->_leaf('term', $1, $attrhash);
+    }
+
+    # Also recognise multi-word terms enclosed in {curly braces}
+    if ($this->{text} =~ s/^{(.*?)}//) {
+       return $this->_leaf('term', $1, $attrhash);
+    }
+
+    my $word = $this->_word();
+    if ($word eq '@attrset') {
+       $attrset = $this->_word();
+       return $this->_parse($attrset, $attrhash);
+
+    } elsif ($word eq '@attr') {
+       $word = $this->_word();
+       if ($word !~ /=/) {
+           $attrset = $word;
+           $word = $this->_word();
+       }
+       my($type, $val) = ($word =~ /(.*)=(.*)/);
+       my %h = %$attrhash;
+       $h{"$attrset:$type"} = $val;
+       return $this->_parse($attrset, \%h);
+
+    } elsif ($word eq '@and' || $word eq '@or' || $word eq '@not') {
+       my $sub1 = $this->_parse($attrset, $attrhash);
+       my $sub2 = $this->_parse($attrset, $attrhash);
+       if ($word eq '@and') {
+           return new Net::Z3950::PQF::AndNode($sub1, $sub2);
+       } elsif ($word eq '@or') {
+           return new Net::Z3950::PQF::OrNode($sub1, $sub2);
+       } elsif ($word eq '@not') {
+           return new Net::Z3950::PQF::NotNode($sub1, $sub2);
+       } else {
+           die "Houston, we have a problem";
+       }
+
+    } elsif ($word eq '@prox') {
+       return $this->_error("proximity not yet implemented");
+
+    } elsif ($word eq '@set') {
+       $word = $this->_word();
+       return $this->_leaf('rset', $word, $attrhash);
+    }
+
+    # It must be a bareword
+    return $this->_leaf('term', $word, $attrhash);
+}
+
+
+# PRIVATE to _parse();
+sub _word {
+    my $this = shift();
+
+    $this->{text} =~ s/^\s+//;
+    $this->{text} =~ s/^(\S+)//;
+    return $1;
+}
+
+
+# PRIVATE to _parse();
+sub _error {
+    my $this = shift();
+    my (@msg) = @_;
+
+    $this->{errmsg} = join("", @msg);
+    return undef;
+}
+
+
+# PRIVATE to _parse();
+sub _leaf {
+    my $this = shift();
+    my($type, $word, $attrhash) = @_;
+
+    my @attrs;
+    foreach my $key (sort keys %$attrhash) {
+       my($attrset, $type) = split /:/, $key;
+       push @attrs, [ $attrset, $type, $attrhash->{$key} ];
+    }
+
+    if ($type eq 'term') {
+       return new Net::Z3950::PQF::TermNode($word, @attrs);
+    } elsif ($type eq 'rset') {
+       return new Net::Z3950::PQF::RsetNode($word, @attrs);
+    } else {
+       die "_leaf() called with type='$type' (should be 'term' or 'rset')";
+    }
 }
 
 
 }
 
 
@@ -101,6 +215,9 @@ sub parse {
 
  print $parser->errmsg();
 
 
  print $parser->errmsg();
 
+Returns the last error-message generated by a failed attempt to parse
+a query.
+
 =cut
 
 sub errmsg {
 =cut
 
 sub errmsg {
@@ -111,6 +228,8 @@ sub errmsg {
 
 =head1 SEE ALSO
 
 
 =head1 SEE ALSO
 
+The C<Net::Z3950::PQF::Node> module.
+
 The definition of the Type-1 query in the Z39.50 standard, the
 relevant section of which is on-line at
 http://www.loc.gov/z3950/agency/markup/09.html#3.7
 The definition of the Type-1 query in the Z39.50 standard, the
 relevant section of which is on-line at
 http://www.loc.gov/z3950/agency/markup/09.html#3.7
@@ -128,7 +247,7 @@ Mike Taylor, E<lt>mike@indexdata.comE<gt>
 Copyright 2004 by Index Data ApS.
 
 This library is free software; you can redistribute it and/or modify
 Copyright 2004 by Index Data ApS.
 
 This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself. 
+it under the same terms as Perl itself.
 
 =cut
 
 
 =cut