eb1a8fec6eb484bb05e5134a19cf004ffb7939da
[perl-pqf.git] / lib / Net / Z3950 / PQF.pm
1 # $Id: PQF.pm,v 1.6 2004-12-20 09:46:58 mike Exp $
2
3 package Net::Z3950::PQF;
4
5 use 5.006;
6 use strict;
7 use warnings;
8
9 use Net::Z3950::PQF::Node;
10
11 our $VERSION = '0.03';
12
13
14 =head1 NAME
15
16 Net::Z3950::PQF - Perl extension for parsing PQF (Prefix Query Format)
17
18 =head1 SYNOPSIS
19
20  use Net::Z3950::PQF;
21  $parser = new Net::Z3950::PQF();
22  $node = $parser->parse('@and @attr 1=1003 kernighan @attr 1=4 unix');
23  print $node->render(0);
24
25 =head1 DESCRIPTION
26
27 This library provides a parser for PQF (Prefix Query Format), an ugly
28 but precise string format for expressing Z39.50 Type-1 queries.  This
29 format is widely used behind the scenes of Z39.50 applications, and is
30 also used extensively with test-harness programs such as the YAZ
31 command-line client, C<yaz-client>.  A few particularly misguided
32 souls have been known to type it by hand.
33
34 Unlike PQF itself, this module
35 is simple to use.  Create a parser object, then pass PQF strings
36 into its C<parse()> method to yield parse-trees.  The trees are made
37 up of nodes whose types are subclasses of
38 C<Net::Z3950::PQF::Node>.
39 and have names of the form
40 C<Net::Z3950::PQF::somethingNode>.  You may find it helpful to use
41 C<Data::Dumper> to visualise the structure of the returned
42 parse-trees.
43
44 What is a PQF parse-tree good for?  Not much.  You can render a
45 human-readable version by invoking the top node's C<render()> method,
46 which is probably useful only for debugging.  If you want to do
47 anything useful, such as implementing an actual query server that
48 understands PQF, you'll have to walk the tree.
49
50 =head1 METHODS
51
52 =head2 new()
53
54  $parser = new Net::Z3950::PQF();
55
56 Creates a new parser object.
57
58 =cut
59
60 sub new {
61     my $class = shift();
62
63     return bless {
64         text => undef,
65         errmsg => undef,
66     }, $class;
67 }
68
69
70 =head2 parse()
71
72  $query = '@and @attr 1=1003 kernighan @attr 1=4 unix';
73  $node = $parser->parse($query);
74  if (!defined $node) {
75      die "parse($query) failed: " . $parser->errmsg();
76  }
77
78 Parses the PQF string provided as its argument.  If an error occurs,
79 then an undefined value is returned, and the error message can be
80 obtained by calling the C<errmsg()> method.  Otherwise, the top node
81 of the parse tree is returned.
82
83  $node2 = $parser->parse($query, "zthes");
84  $node3 = $parser->parse($query, "1.2.840.10003.3.13");
85
86 A second argument may be provided after the query itself.  If it is
87 provided, then it is taken to be either the name or the OID of a
88 default attribute set, which attributes specified in the query belong
89 to if no alternative attribute set is explicitly specified within the
90 query.  When this second argument is absent, the default attribute set
91 is BIB-1.
92
93 =cut
94
95 sub parse {
96     my $this = shift();
97     my($text, $attrset) = @_;
98     $attrset = "bib-1" if !defined $attrset;
99
100     $this->{text} = $text;
101     return $this->_parse($attrset, {});
102 }
103
104
105 # PRIVATE to parse();
106 #
107 # Underlying parse function.  $attrset is the default attribute-set to
108 # use for attributes that are not specified with an explicit set, and
109 # $attrhash is hash of attributes (at most one per type per
110 # attribute-set) to be applied to all nodes below this point.  The
111 # keys of this hash are of the form "<attrset>:<type>" and the values
112 # are the corresponding attribute values.
113 #
114 sub _parse {
115     my $this = shift();
116     my($attrset, $attrhash) = @_;
117
118     ### This rather nasty hack for quoted terms doesn't recognised
119     #   backslash-quoted embedded double quotes.
120     $this->{text} =~ s/^\s+//;
121     if ($this->{text} =~ s/^"(.*?)"//) {
122         return $this->_leaf('term', $1, $attrhash);
123     }
124
125     my $word = $this->_word();
126     if ($word eq '@attrset') {
127         $attrset = $this->_word();
128         return $this->_parse($attrset, $attrhash);
129
130     } elsif ($word eq '@attr') {
131         $word = $this->_word();
132         if ($word !~ /=/) {
133             $attrset = $word;
134             $word = $this->_word();
135         }
136         my($type, $val) = ($word =~ /(.*)=(.*)/);
137         my %h = %$attrhash;
138         $h{"$attrset:$type"} = $val;
139         return $this->_parse($attrset, \%h);
140
141     } elsif ($word eq '@and' || $word eq '@or' || $word eq '@not') {
142         my $sub1 = $this->_parse($attrset, $attrhash);
143         my $sub2 = $this->_parse($attrset, $attrhash);
144         if ($word eq '@and') {
145             return new Net::Z3950::PQF::AndNode($sub1, $sub2);
146         } elsif ($word eq '@or') {
147             return new Net::Z3950::PQF::OrNode($sub1, $sub2);
148         } elsif ($word eq '@not') {
149             return new Net::Z3950::PQF::NotNode($sub1, $sub2);
150         } else {
151             die "Houston, we have a problem";
152         }
153
154     } elsif ($word eq '@prox') {
155         return $this->_error("proximity not yet implemented");
156
157     } elsif ($word eq '@set') {
158         $word = $this->_word();
159         return $this->_leaf('rset', $word, $attrhash);
160     }
161
162     # It must be a bareword
163     return $this->_leaf('term', $word, $attrhash);
164 }
165
166
167 # PRIVATE to _parse();
168 sub _word {
169     my $this = shift();
170
171     $this->{text} =~ s/^\s+//;
172     $this->{text} =~ s/^(\S+)//;
173     return $1;
174 }
175
176
177 # PRIVATE to _parse();
178 sub _error {
179     my $this = shift();
180     my (@msg) = @_;
181
182     $this->{errmsg} = join("", @msg);
183     return undef;
184 }
185
186
187 # PRIVATE to _parse();
188 sub _leaf {
189     my $this = shift();
190     my($type, $word, $attrhash) = @_;
191
192     my @attrs;
193     foreach my $key (sort keys %$attrhash) {
194         my($attrset, $type) = split /:/, $key;
195         push @attrs, [ $attrset, $type, $attrhash->{$key} ];
196     }
197
198     if ($type eq 'term') {
199         return new Net::Z3950::PQF::TermNode($word, @attrs);
200     } elsif ($type eq 'rset') {
201         return new Net::Z3950::PQF::RsetNode($word, @attrs);
202     } else {
203         die "_leaf() called with type='$type' (should be 'term' or 'rset')";
204     }
205 }
206
207
208 =head2 errmsg()
209
210  print $parser->errmsg();
211
212 Returns the last error-message generated by a failed attempt to parse
213 a query.
214
215 =cut
216
217 sub errmsg {
218     my $this = shift();
219     return $this->{errmsg};
220 }
221
222
223 =head1 SEE ALSO
224
225 The C<Net::Z3950::PQF::Node> module.
226
227 The definition of the Type-1 query in the Z39.50 standard, the
228 relevant section of which is on-line at
229 http://www.loc.gov/z3950/agency/markup/09.html#3.7
230
231 The documentation of Prefix Query Format in the YAZ Manual, the
232 relevant section of which is on-line at
233 http://indexdata.com/yaz/doc/tools.tkl#PQF
234
235 =head1 AUTHOR
236
237 Mike Taylor, E<lt>mike@indexdata.comE<gt>
238
239 =head1 COPYRIGHT AND LICENSE
240
241 Copyright 2004 by Index Data ApS.
242
243 This library is free software; you can redistribute it and/or modify
244 it under the same terms as Perl itself.
245
246 =cut
247
248
249 1;