8c97daede8688035fb28f9c17fa285000550cd6e
[ZOOM-Perl-moved-to-github.git] / lib / ZOOM.pm
1 # $Id: ZOOM.pm,v 1.50 2008-05-14 13:33:31 mike Exp $
2
3 use strict;
4 use warnings;
5 use IO::File;
6 use Net::Z3950::ZOOM;
7
8
9 package ZOOM;
10
11 # Member naming convention: hash-element names which begin with an
12 # underscore represent underlying ZOOM-C object descriptors; those
13 # which lack them represent Perl's ZOOM objects.  (The same convention
14 # is used in naming local variables where appropriate.)
15 #
16 # So, for example, the ZOOM::Connection class has an {_conn} element,
17 # which is a pointer to the ZOOM-C Connection object; but the
18 # ZOOM::ResultSet class has a {conn} element, which is a reference to
19 # the Perl-level Connection object by which it was created.  (It may
20 # be that we find we have no need for these references, but for now
21 # they are retained.)
22 #
23 # To get at the underlying ZOOM-C connection object of a result-set
24 # (if you ever needed to do such a thing, which you probably don't)
25 # you'd use $rs->{conn}->_conn().
26
27 # ----------------------------------------------------------------------------
28
29 # The "Error" package contains constants returned as error-codes.
30 package ZOOM::Error;
31 sub NONE { Net::Z3950::ZOOM::ERROR_NONE }
32 sub CONNECT { Net::Z3950::ZOOM::ERROR_CONNECT }
33 sub MEMORY { Net::Z3950::ZOOM::ERROR_MEMORY }
34 sub ENCODE { Net::Z3950::ZOOM::ERROR_ENCODE }
35 sub DECODE { Net::Z3950::ZOOM::ERROR_DECODE }
36 sub CONNECTION_LOST { Net::Z3950::ZOOM::ERROR_CONNECTION_LOST }
37 sub ZINIT { Net::Z3950::ZOOM::ERROR_INIT }
38 sub INTERNAL { Net::Z3950::ZOOM::ERROR_INTERNAL }
39 sub TIMEOUT { Net::Z3950::ZOOM::ERROR_TIMEOUT }
40 sub UNSUPPORTED_PROTOCOL { Net::Z3950::ZOOM::ERROR_UNSUPPORTED_PROTOCOL }
41 sub UNSUPPORTED_QUERY { Net::Z3950::ZOOM::ERROR_UNSUPPORTED_QUERY }
42 sub INVALID_QUERY { Net::Z3950::ZOOM::ERROR_INVALID_QUERY }
43 sub CQL_PARSE { Net::Z3950::ZOOM::ERROR_CQL_PARSE }
44 sub CQL_TRANSFORM { Net::Z3950::ZOOM::ERROR_CQL_TRANSFORM }
45 sub CCL_CONFIG { Net::Z3950::ZOOM::ERROR_CCL_CONFIG }
46 sub CCL_PARSE { Net::Z3950::ZOOM::ERROR_CCL_PARSE }
47 # The following are added specifically for this OO interface
48 sub CREATE_QUERY { 20001 }
49 sub QUERY_CQL { 20002 }
50 sub QUERY_PQF { 20003 }
51 sub SORTBY { 20004 }
52 sub CLONE { 20005 }
53 sub PACKAGE { 20006 }
54 sub SCANTERM { 20007 }
55 sub LOGLEVEL { 20008 }
56
57 # Separate space for CCL errors.  Great.
58 package ZOOM::CCL::Error;
59 sub OK { Net::Z3950::ZOOM::CCL_ERR_OK }
60 sub TERM_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_TERM_EXPECTED }
61 sub RP_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_RP_EXPECTED }
62 sub SETNAME_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_SETNAME_EXPECTED }
63 sub OP_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_OP_EXPECTED }
64 sub BAD_RP { Net::Z3950::ZOOM::CCL_ERR_BAD_RP }
65 sub UNKNOWN_QUAL { Net::Z3950::ZOOM::CCL_ERR_UNKNOWN_QUAL }
66 sub DOUBLE_QUAL { Net::Z3950::ZOOM::CCL_ERR_DOUBLE_QUAL }
67 sub EQ_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_EQ_EXPECTED }
68 sub BAD_RELATION { Net::Z3950::ZOOM::CCL_ERR_BAD_RELATION }
69 sub TRUNC_NOT_LEFT { Net::Z3950::ZOOM::CCL_ERR_TRUNC_NOT_LEFT }
70 sub TRUNC_NOT_BOTH { Net::Z3950::ZOOM::CCL_ERR_TRUNC_NOT_BOTH }
71 sub TRUNC_NOT_RIGHT { Net::Z3950::ZOOM::CCL_ERR_TRUNC_NOT_RIGHT }
72
73 # The "Event" package contains constants returned by last_event()
74 package ZOOM::Event;
75 sub NONE { Net::Z3950::ZOOM::EVENT_NONE }
76 sub CONNECT { Net::Z3950::ZOOM::EVENT_CONNECT }
77 sub SEND_DATA { Net::Z3950::ZOOM::EVENT_SEND_DATA }
78 sub RECV_DATA { Net::Z3950::ZOOM::EVENT_RECV_DATA }
79 sub TIMEOUT { Net::Z3950::ZOOM::EVENT_TIMEOUT }
80 sub UNKNOWN { Net::Z3950::ZOOM::EVENT_UNKNOWN }
81 sub SEND_APDU { Net::Z3950::ZOOM::EVENT_SEND_APDU }
82 sub RECV_APDU { Net::Z3950::ZOOM::EVENT_RECV_APDU }
83 sub RECV_RECORD { Net::Z3950::ZOOM::EVENT_RECV_RECORD }
84 sub RECV_SEARCH { Net::Z3950::ZOOM::EVENT_RECV_SEARCH }
85 sub ZEND { Net::Z3950::ZOOM::EVENT_END }
86
87 # ----------------------------------------------------------------------------
88
89 package ZOOM;
90
91 sub diag_str {
92     my($code) = @_;
93
94     # Special cases for error specific to the OO layer
95     if ($code == ZOOM::Error::CREATE_QUERY) {
96         return "can't create query object";
97     } elsif ($code == ZOOM::Error::QUERY_CQL) {
98         return "can't set CQL query";
99     } elsif ($code == ZOOM::Error::QUERY_PQF) {
100         return "can't set prefix query";
101     } elsif ($code == ZOOM::Error::SORTBY) {
102         return "can't set sort-specification";
103     } elsif ($code == ZOOM::Error::CLONE) {
104         return "can't clone record";
105     } elsif ($code == ZOOM::Error::PACKAGE) {
106         return "can't create package";
107     } elsif ($code == ZOOM::Error::SCANTERM) {
108         return "can't retrieve term from scan-set";
109     } elsif ($code == ZOOM::Error::LOGLEVEL) {
110         return "unregistered log-level";
111     }
112
113     return Net::Z3950::ZOOM::diag_str($code);
114 }
115
116 sub diag_srw_str {
117     my($code) = @_;
118
119     return Net::Z3950::ZOOM::diag_srw_str($code);
120 }
121
122 sub event_str {
123     return Net::Z3950::ZOOM::event_str(@_);
124 }
125
126 sub event {
127     my($connsref) = @_;
128
129     my @_connsref = map { $_->_conn() } @$connsref;
130     return Net::Z3950::ZOOM::event(\@_connsref);
131 }
132
133 sub _oops {
134     my($code, $addinfo, $diagset) = @_;
135
136     die new ZOOM::Exception($code, undef, $addinfo, $diagset);
137 }
138
139 # ----------------------------------------------------------------------------
140
141 package ZOOM::Exception;
142
143 sub new {
144     my $class = shift();
145     my($code, $message, $addinfo, $diagset) = @_;
146
147     $diagset ||= "ZOOM";
148     if (uc($diagset) eq "ZOOM" || uc($diagset) eq "BIB-1") {
149         $message ||= ZOOM::diag_str($code);
150     } elsif (lc($diagset) eq "info:srw/diagnostic/1") {
151         $message ||= ZOOM::diag_srw_str($code);
152     } else {
153         # Should fill in messages for any other known diagsets.
154         $message ||= "(unknown error)";
155     }
156
157     return bless {
158         code => $code,
159         message => $message,
160         addinfo => $addinfo,
161         diagset => $diagset,
162     }, $class;
163 }
164
165 sub code {
166     my $this = shift();
167     return $this->{code};
168 }
169
170 sub message {
171     my $this = shift();
172     return $this->{message};
173 }
174
175 sub addinfo {
176     my $this = shift();
177     return $this->{addinfo};
178 }
179
180 sub diagset {
181     my $this = shift();
182     return $this->{diagset};
183 }
184
185 sub render {
186     my $this = shift();
187
188     my $res = "ZOOM error " . $this->code();
189     $res .= ' "' . $this->message() . '"' if $this->message();
190     $res .= ' (addinfo: "' . $this->addinfo() . '")' if $this->addinfo();
191     $res .= " from diag-set '" . $this->diagset() . "'" if $this->diagset();
192     return $res;
193 }
194
195 # This means that untrapped exceptions render nicely.
196 use overload '""' => \&render;
197
198 # ----------------------------------------------------------------------------
199
200 package ZOOM::Options;
201
202 sub new {
203     my $class = shift();
204     my($p1, $p2) = @_;
205
206     my $opts;
207     if (@_ == 0) {
208         $opts = Net::Z3950::ZOOM::options_create();
209     } elsif (@_ == 1) {
210         $opts = Net::Z3950::ZOOM::options_create_with_parent($p1->_opts());
211     } elsif (@_ == 2) {
212         $opts = Net::Z3950::ZOOM::options_create_with_parent2($p1->_opts(),
213                                                               $p2->_opts());
214     } else {
215         die "can't make $class object with more than 2 parents";
216     }
217
218     return bless {
219         _opts => $opts,
220     }, $class;
221 }
222
223 # PRIVATE to this class and ZOOM::Connection::create() and
224 # ZOOM::Connection::package()
225 #
226 sub _opts {
227     my $this = shift();
228
229     my $_opts = $this->{_opts};
230     die "{_opts} undefined: has this Options block been destroy()ed?"
231         if !defined $_opts;
232
233     return $_opts;
234 }
235
236 sub option {
237     my $this = shift();
238     my($key, $value) = @_;
239
240     my $oldval = Net::Z3950::ZOOM::options_get($this->_opts(), $key);
241     Net::Z3950::ZOOM::options_set($this->_opts(), $key, $value)
242         if defined $value;
243
244     return $oldval;
245 }
246
247 sub option_binary {
248     my $this = shift();
249     my($key, $value) = @_;
250
251     my $dummylen = 0;
252     my $oldval = Net::Z3950::ZOOM::options_getl($this->_opts(),
253                                                 $key, $dummylen);
254     Net::Z3950::ZOOM::options_setl($this->_opts(), $key,
255                                    $value, length($value))
256         if defined $value;
257
258     return $oldval;
259 }
260
261 # This is a bit stupid, since the scalar values that Perl returns from
262 # option() can be used as a boolean; but it's just possible that some
263 # applications will rely on ZOOM_options_get_bool()'s idiosyncratic
264 # interpretation of what constitutes truth.
265 #
266 sub bool {
267     my $this = shift();
268     my($key, $default) = @_;
269
270     return Net::Z3950::ZOOM::options_get_bool($this->_opts(), $key, $default);
271 }
272
273 # .. and the next two are even more stupid
274 sub int {
275     my $this = shift();
276     my($key, $default) = @_;
277
278     return Net::Z3950::ZOOM::options_get_int($this->_opts(), $key, $default);
279 }
280
281 sub set_int {
282     my $this = shift();
283     my($key, $value) = @_;
284
285     Net::Z3950::ZOOM::options_set_int($this->_opts(), $key, $value);
286 }
287
288 #   ### Feel guilty.  Feel very, very guilty.  I've not been able to
289 #       get the callback memory-management right in "ZOOM.xs", with
290 #       the result that the values of $function and $udata passed into
291 #       this function, which are on the stack, have sometimes been
292 #       freed by the time they're used by __ZOOM_option_callback(),
293 #       with hilarious results.  To avoid this, I copy the values into
294 #       module-scoped globals, and pass _those_ into the extension
295 #       function.  To avoid overwriting those globals by subsequent
296 #       calls, I keep all the old ones, pushed onto the @_function and
297 #       @_udata arrays, which means that THIS FUNCTION LEAKS MEMORY
298 #       LIKE IT'S GOING OUT OF FASHION.  Not nice.  One day, I should
299 #       fix this, but for now there's more important fish to fry.
300 #
301 my(@_function, @_udata);
302 sub set_callback {
303     my $o1 = shift();
304     my($function, $udata) = @_;
305
306     push @_function, $function;
307     push @_udata, $udata;
308     Net::Z3950::ZOOM::options_set_callback($o1->_opts(),
309                                            $_function[-1], $_udata[-1]);
310 }
311
312 sub destroy {
313     my $this = shift();
314
315     Net::Z3950::ZOOM::options_destroy($this->_opts());
316     $this->{_opts} = undef;
317 }
318
319
320 # ----------------------------------------------------------------------------
321
322 package ZOOM::Connection;
323
324 sub new {
325     my $class = shift();
326     my($host, $port, @options) = @_;
327
328     my $conn = $class->create(@options);
329     $conn->{host} = $host;
330     $conn->{port} = $port;
331
332     Net::Z3950::ZOOM::connection_connect($conn->_conn(), $host, $port || 0);
333     $conn->_check();
334
335     return $conn;
336 }
337
338 # PRIVATE to this class, to ZOOM::event() and to ZOOM::Query::CQL2RPN::new()
339 sub _conn {
340     my $this = shift();
341
342     my $_conn = $this->{_conn};
343     die "{_conn} undefined: has this Connection been destroy()ed?"
344         if !defined $_conn;
345
346     return $_conn;
347 }
348
349 sub _check {
350     my $this = shift();
351     my($always_die_on_error) = @_;
352
353     my($errcode, $errmsg, $addinfo, $diagset) = (undef, "x", "x", "x");
354     $errcode = Net::Z3950::ZOOM::connection_error_x($this->_conn(), $errmsg,
355                                                     $addinfo, $diagset);
356     if ($errcode) {
357         my $exception = new ZOOM::Exception($errcode, $errmsg, $addinfo,
358                                             $diagset);
359         if (!$this->option("async") || $always_die_on_error) {
360             ZOOM::Log::log("zoom_check", "throwing error $exception");
361             die $exception;
362         } else {
363             ZOOM::Log::log("zoom_check", "not reporting error $exception");
364         }
365     }
366 }
367
368 # This wrapper for _check() is called only from outside the ZOOM
369 # module, and therefore only in situations where an asynchronous
370 # application is actively asking for an exception to be thrown if an
371 # error has been detected.  So it passed always_die_on_error=1 to the
372 # underlying _check() method.
373 #
374 sub check {
375     my $this = shift();
376     return $this->_check(1);
377 }
378
379 sub create {
380     my $class = shift();
381     my(@options) = @_;
382
383     my $_opts;
384     if (@_ == 1) {
385         $_opts = $_[0]->_opts();
386     } else {
387         $_opts = Net::Z3950::ZOOM::options_create();
388         while (@options >= 2) {
389             my $key = shift(@options);
390             my $val = shift(@options);
391             Net::Z3950::ZOOM::options_set($_opts, $key, $val);
392         }
393
394         die "Odd number of options specified"
395             if @options;
396     }
397
398     my $_conn = Net::Z3950::ZOOM::connection_create($_opts);
399     my $conn = bless {
400         host => undef,
401         port => undef,
402         _conn => $_conn,
403     }, $class;
404     return $conn;
405 }
406
407 sub error_x {
408     my $this = shift();
409
410     my($errcode, $errmsg, $addinfo, $diagset) = (undef, "dummy", "dummy", "d");
411     $errcode = Net::Z3950::ZOOM::connection_error_x($this->_conn(), $errmsg,
412                                                     $addinfo, $diagset);
413     return wantarray() ? ($errcode, $errmsg, $addinfo, $diagset) : $errcode;
414 }
415
416 sub exception {
417     my $this = shift();
418
419     my($errcode, $errmsg, $addinfo, $diagset) = $this->error_x();
420     return undef if $errcode == 0;
421     return new ZOOM::Exception($errcode, $errmsg, $addinfo, $diagset);
422 }
423
424 sub errcode {
425     my $this = shift();
426     return Net::Z3950::ZOOM::connection_errcode($this->_conn());
427 }
428
429 sub errmsg {
430     my $this = shift();
431     return Net::Z3950::ZOOM::connection_errmsg($this->_conn());
432 }
433
434 sub addinfo {
435     my $this = shift();
436     return Net::Z3950::ZOOM::connection_addinfo($this->_conn());
437 }
438
439 sub diagset {
440     my $this = shift();
441     return Net::Z3950::ZOOM::connection_diagset($this->_conn());
442 }
443
444 sub connect {
445     my $this = shift();
446     my($host, $port) = @_;
447
448     $port = 0 if !defined $port;
449     Net::Z3950::ZOOM::connection_connect($this->_conn(), $host, $port);
450     $this->_check();
451     # No return value
452 }
453
454 sub option {
455     my $this = shift();
456     my($key, $value) = @_;
457
458     my $oldval = Net::Z3950::ZOOM::connection_option_get($this->_conn(), $key);
459     Net::Z3950::ZOOM::connection_option_set($this->_conn(), $key, $value)
460         if defined $value;
461
462     return $oldval;
463 }
464
465 sub option_binary {
466     my $this = shift();
467     my($key, $value) = @_;
468
469     my $dummylen = 0;
470     my $oldval = Net::Z3950::ZOOM::connection_option_getl($this->_conn(),
471                                                           $key, $dummylen);
472     Net::Z3950::ZOOM::connection_option_setl($this->_conn(), $key,
473                                              $value, length($value))
474         if defined $value;
475
476     return $oldval;
477 }
478
479 sub search {
480     my $this = shift();
481     my($query) = @_;
482
483     my $_rs = Net::Z3950::ZOOM::connection_search($this->_conn(),
484                                                   $query->_query());
485     $this->_check();
486     return _new ZOOM::ResultSet($this, $query, $_rs);
487 }
488
489 sub search_pqf {
490     my $this = shift();
491     my($pqf) = @_;
492
493     my $_rs = Net::Z3950::ZOOM::connection_search_pqf($this->_conn(), $pqf);
494     $this->_check();
495     return _new ZOOM::ResultSet($this, $pqf, $_rs);
496 }
497
498 sub scan_pqf {
499     my $this = shift();
500     my($startterm) = @_;
501
502     my $_ss = Net::Z3950::ZOOM::connection_scan($this->_conn(), $startterm);
503     $this->_check();
504     return _new ZOOM::ScanSet($this, $startterm, $_ss);
505 }
506
507 sub scan {
508     my $this = shift();
509     my($query) = @_;
510
511     my $_ss = Net::Z3950::ZOOM::connection_scan1($this->_conn(),
512                                                  $query->_query());
513     $this->_check();
514     return _new ZOOM::ScanSet($this, $query, $_ss);
515 }
516
517 sub package {
518     my $this = shift();
519     my($options) = @_;
520
521     my $_o = defined $options ? $options->_opts() :
522         Net::Z3950::ZOOM::options_create();
523     my $_p = Net::Z3950::ZOOM::connection_package($this->_conn(), $_o)
524         or ZOOM::_oops(ZOOM::Error::PACKAGE);
525
526     return _new ZOOM::Package($this, $options, $_p);
527 }
528
529 sub last_event {
530     my $this = shift();
531
532     return Net::Z3950::ZOOM::connection_last_event($this->_conn());
533 }
534
535 sub is_idle {
536     my $this = shift();
537
538     return Net::Z3950::ZOOM::connection_is_idle($this->_conn());
539 }
540
541 sub peek_event {
542     my $this = shift();
543
544     return Net::Z3950::ZOOM::connection_peek_event($this->_conn());
545 }
546
547 sub destroy {
548     my $this = shift();
549
550     Net::Z3950::ZOOM::connection_destroy($this->_conn());
551     $this->{_conn} = undef;
552 }
553
554
555 # ----------------------------------------------------------------------------
556
557 package ZOOM::Query;
558
559 sub new {
560     my $class = shift();
561     die "You can't create $class objects: it's a virtual base class";
562 }
563
564 # PRIVATE to this class and ZOOM::Connection::search()
565 sub _query {
566     my $this = shift();
567
568     my $_query = $this->{_query};
569     die "{_query} undefined: has this Query been destroy()ed?"
570         if !defined $_query;
571
572     return $_query;
573 }
574
575 sub sortby {
576     my $this = shift();
577     my($sortby) = @_;
578
579     Net::Z3950::ZOOM::query_sortby($this->_query(), $sortby) == 0
580         or ZOOM::_oops(ZOOM::Error::SORTBY, $sortby);
581 }
582
583 sub destroy {
584     my $this = shift();
585
586     Net::Z3950::ZOOM::query_destroy($this->_query());
587     $this->{_query} = undef;
588 }
589
590
591 package ZOOM::Query::CQL;
592 our @ISA = qw(ZOOM::Query);
593
594 sub new {
595     my $class = shift();
596     my($string) = @_;
597
598     my $q = Net::Z3950::ZOOM::query_create()
599         or ZOOM::_oops(ZOOM::Error::CREATE_QUERY);
600     Net::Z3950::ZOOM::query_cql($q, $string) == 0
601         or ZOOM::_oops(ZOOM::Error::QUERY_CQL, $string);
602
603     return bless {
604         _query => $q,
605     }, $class;
606 }
607
608
609 package ZOOM::Query::CQL2RPN;
610 our @ISA = qw(ZOOM::Query);
611
612 sub new {
613     my $class = shift();
614     my($string, $conn) = @_;
615
616     my $q = Net::Z3950::ZOOM::query_create()
617         or ZOOM::_oops(ZOOM::Error::CREATE_QUERY);
618     # check() throws the exception we want; but we only want it on failure!
619     Net::Z3950::ZOOM::query_cql2rpn($q, $string, $conn->_conn()) == 0
620         or $conn->_check();
621
622     return bless {
623         _query => $q,
624     }, $class;
625 }
626
627
628 # We have to work around the retarded ZOOM_query_ccl2rpn() API
629 package ZOOM::Query::CCL2RPN;
630 our @ISA = qw(ZOOM::Query);
631
632 sub new {
633     my $class = shift();
634     my($string, $conn) = @_;
635
636     my $q = Net::Z3950::ZOOM::query_create()
637         or ZOOM::_oops(ZOOM::Error::CREATE_QUERY);
638
639     my $config = $conn->option("cclqual");
640     if (!defined $config) {
641         my $cclfile = $conn->option("cclfile")
642             or ZOOM::_oops(ZOOM::Error::CCL_CONFIG,
643                            "no 'cclqual' or 'cclfile' specified");
644         my $fh = new IO::File("<$cclfile")
645             or ZOOM::_oops(ZOOM::Error::CCL_CONFIG,
646                            "can't open cclfile '$cclfile': $!");
647         $config = join("", <$fh>);
648         $fh->close();
649     }
650
651     my($ccl_errcode, $ccl_errstr, $ccl_errpos) = (0, "", 0);
652     if (Net::Z3950::ZOOM::query_ccl2rpn($q, $string, $config,
653                                         $ccl_errcode, $ccl_errstr,
654                                         $ccl_errpos) < 0) {
655         # We have no use for $ccl_errcode or $ccl_errpos
656         ZOOM::_oops(ZOOM::Error::CCL_PARSE, $ccl_errstr);
657     }
658
659     return bless {
660         _query => $q,
661     }, $class;
662 }
663
664
665 package ZOOM::Query::PQF;
666 our @ISA = qw(ZOOM::Query);
667
668 sub new {
669     my $class = shift();
670     my($string) = @_;
671
672     my $q = Net::Z3950::ZOOM::query_create()
673         or ZOOM::_oops(ZOOM::Error::CREATE_QUERY);
674     Net::Z3950::ZOOM::query_prefix($q, $string) == 0
675         or ZOOM::_oops(ZOOM::Error::QUERY_PQF, $string);
676
677     return bless {
678         _query => $q,
679     }, $class;
680 }
681
682
683 # ----------------------------------------------------------------------------
684
685 package ZOOM::ResultSet;
686
687 sub new {
688     my $class = shift();
689     die "You can't create $class objects directly";
690 }
691
692 # PRIVATE to ZOOM::Connection::search() and ZOOM::Connection::search_pqf()
693 sub _new {
694     my $class = shift();
695     my($conn, $query, $_rs) = @_;
696
697     return bless {
698         conn => $conn,
699         query => $query,        # This is not currently used, which is
700                                 # just as well since it could be
701                                 # either a string (when the RS is
702                                 # created with search_pqf()) or a
703                                 # ZOOM::Query object (when it's
704                                 # created with search())
705         _rs => $_rs,
706     }, $class;
707 }
708
709 # PRIVATE to this class
710 sub _rs {
711     my $this = shift();
712
713     my $_rs = $this->{_rs};
714     die "{_rs} undefined: has this ResultSet been destroy()ed?"
715         if !defined $_rs;
716
717     return $_rs;
718 }
719
720 sub option {
721     my $this = shift();
722     my($key, $value) = @_;
723
724     my $oldval = Net::Z3950::ZOOM::resultset_option_get($this->_rs(), $key);
725     Net::Z3950::ZOOM::resultset_option_set($this->_rs(), $key, $value)
726         if defined $value;
727
728     return $oldval;
729 }
730
731 sub size {
732     my $this = shift();
733
734     return Net::Z3950::ZOOM::resultset_size($this->_rs());
735 }
736
737 sub record {
738     my $this = shift();
739     my($which) = @_;
740
741     my $_rec = Net::Z3950::ZOOM::resultset_record($this->_rs(), $which);
742     $this->{conn}->_check();
743
744     # Even if no error has occurred, I think record() might
745     # legitimately return undef if we're running in asynchronous mode
746     # and the record just hasn't been retrieved yet.  This goes double
747     # for record_immediate().
748     return undef if !defined $_rec;
749
750     # For some reason, I have to use the explicit "->" syntax in order
751     # to invoke the ZOOM::Record constructor here, even though I don't
752     # have to do the same for _new ZOOM::ResultSet above.  Weird.
753     return ZOOM::Record->_new($this, $which, $_rec);
754 }
755
756 sub record_immediate {
757     my $this = shift();
758     my($which) = @_;
759
760     my $_rec = Net::Z3950::ZOOM::resultset_record_immediate($this->_rs(),
761                                                             $which);
762     $this->{conn}->_check();
763     # The record might legitimately not be there yet
764     return undef if !defined $_rec;
765
766     return ZOOM::Record->_new($this, $which, $_rec);
767 }
768
769 sub cache_reset {
770     my $this = shift();
771
772     Net::Z3950::ZOOM::resultset_cache_reset($this->_rs());
773 }
774
775 sub records {
776     my $this = shift();
777     my($start, $count, $return_records) = @_;
778
779     # If the request is out of range, ZOOM-C will currently (as of YAZ
780     # 2.1.38) no-op: it understandably refuses to build and send a
781     # known-bad APDU, but it doesn't set a diagnostic as it ought.  So
782     # for now, we do it here.  It would be more polite to stash the
783     # error-code in the ZOOM-C connection object for subsequent
784     # discovery (which is what ZOOM-C will presumably do itself when
785     # it's fixed) but since there is no API that allows us to do that,
786     # we just have to throw the exception right now.  That's probably
787     # OK for synchronous applications, but not really for
788     # multiplexers.
789     my $size = $this->size();
790     if ($start + $count-1 >= $size) {
791         # BIB-1 diagnostic 13 is "Present request out-of-range"
792         ZOOM::_oops(13, undef, "BIB-1");
793     }
794
795     my $raw = Net::Z3950::ZOOM::resultset_records($this->_rs(), $start, $count,
796                                                   $return_records);
797     # By design, $raw may be undefined (if $return_records is true)
798     return undef if !defined $raw;
799
800     # We need to package up the returned records in ZOOM::Record objects
801     my @res = ();
802     for my $i (0 .. @$raw-1) {
803         my $_rec = $raw->[$i];
804         if (!defined $_rec) {
805             push @res, undef;
806         } else {
807             push @res, ZOOM::Record->_new($this, $start+$i, $_rec);
808         }
809     }
810
811     return \@res;
812 }
813
814 sub sort {
815     my $this = shift();
816     my($sort_type, $sort_spec) = @_;
817
818     return Net::Z3950::ZOOM::resultset_sort1($this->_rs(),
819                                              $sort_type, $sort_spec);
820 }
821
822 sub destroy {
823     my $this = shift();
824
825     Net::Z3950::ZOOM::resultset_destroy($this->_rs());
826     $this->{_rs} = undef;
827 }
828
829
830 # ----------------------------------------------------------------------------
831
832 package ZOOM::Record;
833
834 sub new {
835     my $class = shift();
836     die "You can't create $class objects directly";
837 }
838
839 # PRIVATE to ZOOM::ResultSet::record(),
840 # ZOOM::ResultSet::record_immediate(), ZOOM::ResultSet::records() and
841 # ZOOM::Record::clone()
842 #
843 sub _new {
844     my $class = shift();
845     my($rs, $which, $_rec) = @_;
846
847     return bless {
848         rs => $rs,
849         which => $which,
850         _rec => $_rec,
851     }, $class;
852 }
853
854 # PRIVATE to this class
855 sub _rec {
856     my $this = shift();
857
858     my $_rec = $this->{_rec};
859     die "{_rec} undefined: has this Record been destroy()ed?"
860         if !defined $_rec;
861
862     return $_rec;
863 }
864
865 sub error {
866     my $this = shift();
867
868     my($errcode, $errmsg, $addinfo, $diagset) = (undef, "dummy", "dummy", "d");
869     $errcode = Net::Z3950::ZOOM::record_error($this->_rec(), $errmsg,
870                                               $addinfo, $diagset);
871
872     return wantarray() ? ($errcode, $errmsg, $addinfo, $diagset) : $errcode;
873 }
874
875 sub exception {
876     my $this = shift();
877
878     my($errcode, $errmsg, $addinfo, $diagset) = $this->error();
879     return undef if $errcode == 0;
880     return new ZOOM::Exception($errcode, $errmsg, $addinfo, $diagset);
881 }
882
883
884 sub render {
885     my $this = shift();
886
887     return $this->get("render", @_);
888 }
889
890 sub raw {
891     my $this = shift();
892
893     return $this->get("raw", @_);
894 }
895
896 sub get {
897     my $this = shift();
898     my($type, $args) = @_;
899
900     $type = "$type;$args" if defined $args;
901     return Net::Z3950::ZOOM::record_get($this->_rec(), $type);
902 }
903
904 sub clone {
905     my $this = shift();
906
907     my $raw = Net::Z3950::ZOOM::record_clone($this->_rec())
908         or ZOOM::_oops(ZOOM::Error::CLONE);
909
910     # Arg 1 (rs) is undefined as the new record doesn't belong to an RS
911     return _new ZOOM::Record(undef, undef, $raw);
912 }
913
914 sub destroy {
915     my $this = shift();
916
917     Net::Z3950::ZOOM::record_destroy($this->_rec());
918     $this->{_rec} = undef;
919 }
920
921
922 # ----------------------------------------------------------------------------
923
924 package ZOOM::ScanSet;
925
926 sub new {
927     my $class = shift();
928     die "You can't create $class objects directly";
929 }
930
931 # PRIVATE to ZOOM::Connection::scan(),
932 sub _new {
933     my $class = shift();
934     my($conn, $startterm, $_ss) = @_;
935
936     return bless {
937         conn => $conn,
938         startterm => $startterm,# This is not currently used, which is
939                                 # just as well since it could be
940                                 # either a string (when the SS is
941                                 # created with scan()) or a
942                                 # ZOOM::Query object (when it's
943                                 # created with scan1())
944         _ss => $_ss,
945     }, $class;
946 }
947
948 # PRIVATE to this class
949 sub _ss {
950     my $this = shift();
951
952     my $_ss = $this->{_ss};
953     die "{_ss} undefined: has this ScanSet been destroy()ed?"
954         if !defined $_ss;
955
956     return $_ss;
957 }
958
959 sub option {
960     my $this = shift();
961     my($key, $value) = @_;
962
963     my $oldval = Net::Z3950::ZOOM::scanset_option_get($this->_ss(), $key);
964     Net::Z3950::ZOOM::scanset_option_set($this->_ss(), $key, $value)
965         if defined $value;
966
967     return $oldval;
968 }
969
970 sub size {
971     my $this = shift();
972
973     return Net::Z3950::ZOOM::scanset_size($this->_ss());
974 }
975
976 sub term {
977     my $this = shift();
978     my($which) = @_;
979
980     my($occ, $len) = (0, 0);
981     my $term = Net::Z3950::ZOOM::scanset_term($this->_ss(), $which,
982                                               $occ, $len)
983         or ZOOM::_oops(ZOOM::Error::SCANTERM);
984
985     die "length of term '$term' differs from returned len=$len"
986         if length($term) != $len;
987
988     return ($term, $occ);
989 }
990
991 sub display_term {
992     my $this = shift();
993     my($which) = @_;
994
995     my($occ, $len) = (0, 0);
996     my $term = Net::Z3950::ZOOM::scanset_display_term($this->_ss(), $which,
997                                                       $occ, $len)
998         or ZOOM::_oops(ZOOM::Error::SCANTERM);
999
1000     die "length of display term '$term' differs from returned len=$len"
1001         if length($term) != $len;
1002
1003     return ($term, $occ);
1004 }
1005
1006 sub destroy {
1007     my $this = shift();
1008
1009     Net::Z3950::ZOOM::scanset_destroy($this->_ss());
1010     $this->{_ss} = undef;
1011 }
1012
1013
1014 # ----------------------------------------------------------------------------
1015
1016 package ZOOM::Package;
1017
1018 sub new {
1019     my $class = shift();
1020     die "You can't create $class objects directly";
1021 }
1022
1023 # PRIVATE to ZOOM::Connection::package(),
1024 sub _new {
1025     my $class = shift();
1026     my($conn, $options, $_p) = @_;
1027
1028     return bless {
1029         conn => $conn,
1030         options => $options,
1031         _p => $_p,
1032     }, $class;
1033 }
1034
1035 # PRIVATE to this class
1036 sub _p {
1037     my $this = shift();
1038
1039     my $_p = $this->{_p};
1040     die "{_p} undefined: has this Package been destroy()ed?"
1041         if !defined $_p;
1042
1043     return $_p;
1044 }
1045
1046 sub option {
1047     my $this = shift();
1048     my($key, $value) = @_;
1049
1050     my $oldval = Net::Z3950::ZOOM::package_option_get($this->_p(), $key);
1051     Net::Z3950::ZOOM::package_option_set($this->_p(), $key, $value)
1052         if defined $value;
1053
1054     return $oldval;
1055 }
1056
1057 sub send {
1058     my $this = shift();
1059     my($type) = @_;
1060
1061     Net::Z3950::ZOOM::package_send($this->_p(), $type);
1062     $this->{conn}->_check();
1063 }
1064
1065 sub destroy {
1066     my $this = shift();
1067
1068     Net::Z3950::ZOOM::package_destroy($this->_p());
1069     $this->{_p} = undef;
1070 }
1071
1072
1073 # There follows trivial support for YAZ logging.  This is wired out
1074 # into the Net::Z3950::ZOOM package, and we here provide wrapper
1075 # functions -- nothing more than aliases, really -- in the ZOOM::Log
1076 # package.  There really is no point in inventing an OO interface.
1077 #
1078 # Passing @_ directly to the underlying Net::Z3950::ZOOM::* functions
1079 # doesn't work, for reasons that I can't begin to fathom, and that
1080 # don't particularly interest me.  Unpacking into scalars and passing
1081 # those _does_ work, so that's what we do.
1082
1083 package ZOOM::Log;
1084
1085 sub mask_str      { my($a) = @_; Net::Z3950::ZOOM::yaz_log_mask_str($a); }
1086 sub module_level  { my($a) = @_; Net::Z3950::ZOOM::yaz_log_module_level($a); }
1087 sub init          { my($a, $b, $c) = @_;
1088                     Net::Z3950::ZOOM::yaz_log_init($a, $b, $c) }
1089 sub init_file     { my($a) = @_; Net::Z3950::ZOOM::yaz_log_init_file($a) }
1090 sub init_level    { my($a) = @_; Net::Z3950::ZOOM::yaz_log_init_level($a) }
1091 sub init_prefix   { my($a) = @_; Net::Z3950::ZOOM::yaz_log_init_prefix($a) }
1092 sub time_format   { my($a) = @_; Net::Z3950::ZOOM::yaz_log_time_format($a) }
1093 sub init_max_size { my($a) = @_; Net::Z3950::ZOOM::yaz_log_init_max_size($a) }
1094
1095 sub log {
1096     my($level, @message) = @_;
1097
1098     if ($level !~ /^(0x)?\d+$/) {
1099         # Assuming its log-level name, we look it up.
1100         my $num = module_level($level);
1101         ZOOM::_oops(ZOOM::Error::LOGLEVEL, $level)
1102             if $num == 0;
1103         $level = $num;
1104     }
1105
1106     Net::Z3950::ZOOM::yaz_log($level, join("", @message));
1107 }
1108
1109 BEGIN { ZOOM::Log::mask_str("zoom_check"); }
1110
1111 1;