+# Separate space for CCL errors. Great.
+package ZOOM::CCL::Error;
+sub OK { Net::Z3950::ZOOM::CCL_ERR_OK }
+sub TERM_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_TERM_EXPECTED }
+sub RP_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_RP_EXPECTED }
+sub SETNAME_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_SETNAME_EXPECTED }
+sub OP_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_OP_EXPECTED }
+sub BAD_RP { Net::Z3950::ZOOM::CCL_ERR_BAD_RP }
+sub UNKNOWN_QUAL { Net::Z3950::ZOOM::CCL_ERR_UNKNOWN_QUAL }
+sub DOUBLE_QUAL { Net::Z3950::ZOOM::CCL_ERR_DOUBLE_QUAL }
+sub EQ_EXPECTED { Net::Z3950::ZOOM::CCL_ERR_EQ_EXPECTED }
+sub BAD_RELATION { Net::Z3950::ZOOM::CCL_ERR_BAD_RELATION }
+sub TRUNC_NOT_LEFT { Net::Z3950::ZOOM::CCL_ERR_TRUNC_NOT_LEFT }
+sub TRUNC_NOT_BOTH { Net::Z3950::ZOOM::CCL_ERR_TRUNC_NOT_BOTH }
+sub TRUNC_NOT_RIGHT { Net::Z3950::ZOOM::CCL_ERR_TRUNC_NOT_RIGHT }
+
+# The "Event" package contains constants returned by last_event()
+package ZOOM::Event;
+sub NONE { Net::Z3950::ZOOM::EVENT_NONE }
+sub CONNECT { Net::Z3950::ZOOM::EVENT_CONNECT }
+sub SEND_DATA { Net::Z3950::ZOOM::EVENT_SEND_DATA }
+sub RECV_DATA { Net::Z3950::ZOOM::EVENT_RECV_DATA }
+sub TIMEOUT { Net::Z3950::ZOOM::EVENT_TIMEOUT }
+sub UNKNOWN { Net::Z3950::ZOOM::EVENT_UNKNOWN }
+sub SEND_APDU { Net::Z3950::ZOOM::EVENT_SEND_APDU }
+sub RECV_APDU { Net::Z3950::ZOOM::EVENT_RECV_APDU }
+sub RECV_RECORD { Net::Z3950::ZOOM::EVENT_RECV_RECORD }
+sub RECV_SEARCH { Net::Z3950::ZOOM::EVENT_RECV_SEARCH }
+sub ZEND { Net::Z3950::ZOOM::EVENT_END }
+
+# ----------------------------------------------------------------------------
+
+package ZOOM;
+
+sub diag_str {
+ my($code) = @_;
+
+ # Special cases for error specific to the OO layer
+ if ($code == ZOOM::Error::CREATE_QUERY) {
+ return "can't create query object";
+ } elsif ($code == ZOOM::Error::QUERY_CQL) {
+ return "can't set CQL query";
+ } elsif ($code == ZOOM::Error::QUERY_PQF) {
+ return "can't set prefix query";
+ } elsif ($code == ZOOM::Error::SORTBY) {
+ return "can't set sort-specification";
+ } elsif ($code == ZOOM::Error::CLONE) {
+ return "can't clone record";
+ } elsif ($code == ZOOM::Error::PACKAGE) {
+ return "can't create package";
+ } elsif ($code == ZOOM::Error::SCANTERM) {
+ return "can't retrieve term from scan-set";
+ } elsif ($code == ZOOM::Error::LOGLEVEL) {
+ return "unregistered log-level";
+ }
+
+ return Net::Z3950::ZOOM::diag_str($code);
+}
+
+sub diag_srw_str {
+ my($code) = @_;
+
+ return Net::Z3950::ZOOM::diag_srw_str($code);
+}
+
+sub event_str {
+ return Net::Z3950::ZOOM::event_str(@_);
+}
+
+sub event {
+ my($connsref) = @_;
+
+ my @_connsref = map { $_->_conn() } @$connsref;
+ return Net::Z3950::ZOOM::event(\@_connsref);
+}
+
+sub _oops {
+ my($code, $addinfo, $diagset) = @_;
+
+ die new ZOOM::Exception($code, undef, $addinfo, $diagset);
+}
+
+# ----------------------------------------------------------------------------
+
+package ZOOM::Exception;
+
+sub new {
+ my $class = shift();
+ my($code, $message, $addinfo, $diagset) = @_;
+
+ $diagset ||= "ZOOM";
+ if (uc($diagset) eq "ZOOM" || uc($diagset) eq "BIB-1") {
+ $message ||= ZOOM::diag_str($code);
+ } elsif (lc($diagset) eq "info:srw/diagnostic/1") {
+ $message ||= ZOOM::diag_srw_str($code);
+ } else {
+ # Should fill in messages for any other known diagsets.
+ $message ||= "(unknown error)";
+ }
+
+ return bless {
+ code => $code,
+ message => $message,
+ addinfo => $addinfo,
+ diagset => $diagset,
+ }, $class;
+}
+
+sub code {
+ my $this = shift();
+ return $this->{code};
+}
+
+sub message {
+ my $this = shift();
+ return $this->{message};
+}
+
+sub addinfo {
+ my $this = shift();
+ return $this->{addinfo};
+}
+
+sub diagset {
+ my $this = shift();
+ return $this->{diagset};
+}
+
+sub render {
+ my $this = shift();
+
+ my $res = "ZOOM error " . $this->code();
+ $res .= ' "' . $this->message() . '"' if $this->message();
+ $res .= ' (addinfo: "' . $this->addinfo() . '")' if $this->addinfo();
+ $res .= " from diag-set '" . $this->diagset() . "'" if $this->diagset();
+ return $res;
+}
+
+# This means that untrapped exceptions render nicely.
+use overload '""' => \&render;
+
+# ----------------------------------------------------------------------------
+
+package ZOOM::Options;
+
+sub new {
+ my $class = shift();
+ my($p1, $p2) = @_;
+
+ my $opts;
+ if (@_ == 0) {
+ $opts = Net::Z3950::ZOOM::options_create();
+ } elsif (@_ == 1) {
+ $opts = Net::Z3950::ZOOM::options_create_with_parent($p1->_opts());
+ } elsif (@_ == 2) {
+ $opts = Net::Z3950::ZOOM::options_create_with_parent2($p1->_opts(),
+ $p2->_opts());
+ } else {
+ die "can't make $class object with more than 2 parents";
+ }
+
+ return bless {
+ _opts => $opts,
+ }, $class;
+}
+
+# PRIVATE to this class and ZOOM::Connection::create() and
+# ZOOM::Connection::package()
+#
+sub _opts {
+ my $this = shift();
+
+ my $_opts = $this->{_opts};
+ die "{_opts} undefined: has this Options block been destroy()ed?"
+ if !defined $_opts;
+
+ return $_opts;
+}
+
+sub option {
+ my $this = shift();
+ my($key, $value) = @_;
+
+ my $oldval = Net::Z3950::ZOOM::options_get($this->_opts(), $key);
+ Net::Z3950::ZOOM::options_set($this->_opts(), $key, $value)
+ if defined $value;
+
+ return $oldval;
+}
+
+sub option_binary {
+ my $this = shift();
+ my($key, $value) = @_;
+
+ my $dummylen = 0;
+ my $oldval = Net::Z3950::ZOOM::options_getl($this->_opts(),
+ $key, $dummylen);
+ Net::Z3950::ZOOM::options_setl($this->_opts(), $key,
+ $value, length($value))
+ if defined $value;
+
+ return $oldval;
+}
+
+# This is a bit stupid, since the scalar values that Perl returns from
+# option() can be used as a boolean; but it's just possible that some
+# applications will rely on ZOOM_options_get_bool()'s idiosyncratic
+# interpretation of what constitutes truth.
+#
+sub bool {
+ my $this = shift();
+ my($key, $default) = @_;
+
+ return Net::Z3950::ZOOM::options_get_bool($this->_opts(), $key, $default);
+}
+
+# .. and the next two are even more stupid
+sub int {
+ my $this = shift();
+ my($key, $default) = @_;
+
+ return Net::Z3950::ZOOM::options_get_int($this->_opts(), $key, $default);
+}
+
+sub set_int {
+ my $this = shift();
+ my($key, $value) = @_;
+
+ Net::Z3950::ZOOM::options_set_int($this->_opts(), $key, $value);
+}
+
+# ### Feel guilty. Feel very, very guilty. I've not been able to
+# get the callback memory-management right in "ZOOM.xs", with
+# the result that the values of $function and $udata passed into
+# this function, which are on the stack, have sometimes been
+# freed by the time they're used by __ZOOM_option_callback(),
+# with hilarious results. To avoid this, I copy the values into
+# module-scoped globals, and pass _those_ into the extension
+# function. To avoid overwriting those globals by subsequent
+# calls, I keep all the old ones, pushed onto the @_function and
+# @_udata arrays, which means that THIS FUNCTION LEAKS MEMORY
+# LIKE IT'S GOING OUT OF FASHION. Not nice. One day, I should
+# fix this, but for now there's more important fish to fry.
+#
+my(@_function, @_udata);
+sub set_callback {
+ my $o1 = shift();
+ my($function, $udata) = @_;
+
+ push @_function, $function;
+ push @_udata, $udata;
+ Net::Z3950::ZOOM::options_set_callback($o1->_opts(),
+ $_function[-1], $_udata[-1]);
+}
+
+sub destroy {
+ my $this = shift();
+
+ Net::Z3950::ZOOM::options_destroy($this->_opts());
+ $this->{_opts} = undef;
+}
+
+
+# ----------------------------------------------------------------------------
+
+package ZOOM::Connection;
+
+sub new {
+ my $class = shift();
+ my($host, $port, @options) = @_;
+
+ my $conn = $class->create(@options);
+ $conn->{host} = $host;
+ $conn->{port} = $port;
+
+ Net::Z3950::ZOOM::connection_connect($conn->_conn(), $host, $port || 0);
+ $conn->_check();
+
+ return $conn;
+}
+
+# PRIVATE to this class, to ZOOM::event() and to ZOOM::Query::CQL2RPN::new()
+sub _conn {
+ my $this = shift();
+
+ my $_conn = $this->{_conn};
+ die "{_conn} undefined: has this Connection been destroy()ed?"
+ if !defined $_conn;
+
+ return $_conn;
+}
+
+sub _check {
+ my $this = shift();
+ my($always_die_on_error) = @_;
+
+ my($errcode, $errmsg, $addinfo, $diagset) = (undef, "x", "x", "x");
+ $errcode = Net::Z3950::ZOOM::connection_error_x($this->_conn(), $errmsg,
+ $addinfo, $diagset);
+ if ($errcode) {
+ my $exception = new ZOOM::Exception($errcode, $errmsg, $addinfo,
+ $diagset);
+ if (!$this->option("async") || $always_die_on_error) {
+ ZOOM::Log::log("zoom_check", "throwing error $exception");
+ die $exception;
+ } else {
+ ZOOM::Log::log("zoom_check", "not reporting error $exception");
+ }
+ }
+}
+
+# This wrapper for _check() is called only from outside the ZOOM
+# module, and therefore only in situations where an asynchronous
+# application is actively asking for an exception to be thrown if an
+# error has been detected. So it passed always_die_on_error=1 to the
+# underlying _check() method.
+#
+sub check {
+ my $this = shift();
+ return $this->_check(1);
+}
+
+sub create {
+ my $class = shift();
+ my(@options) = @_;
+
+ my $_opts;
+ if (@_ == 1) {
+ $_opts = $_[0]->_opts();
+ } else {
+ $_opts = Net::Z3950::ZOOM::options_create();
+ while (@options >= 2) {
+ my $key = shift(@options);
+ my $val = shift(@options);
+ Net::Z3950::ZOOM::options_set($_opts, $key, $val);
+ }
+
+ die "Odd number of options specified"
+ if @options;
+ }
+
+ my $_conn = Net::Z3950::ZOOM::connection_create($_opts);
+ my $conn = bless {
+ host => undef,
+ port => undef,
+ _conn => $_conn,
+ }, $class;
+ return $conn;
+}
+
+sub error_x {
+ my $this = shift();
+
+ my($errcode, $errmsg, $addinfo, $diagset) = (undef, "dummy", "dummy", "d");
+ $errcode = Net::Z3950::ZOOM::connection_error_x($this->_conn(), $errmsg,
+ $addinfo, $diagset);
+ return wantarray() ? ($errcode, $errmsg, $addinfo, $diagset) : $errcode;
+}
+
+sub exception {
+ my $this = shift();
+
+ my($errcode, $errmsg, $addinfo, $diagset) = $this->error_x();
+ return undef if $errcode == 0;
+ return new ZOOM::Exception($errcode, $errmsg, $addinfo, $diagset);
+}
+
+sub errcode {
+ my $this = shift();
+ return Net::Z3950::ZOOM::connection_errcode($this->_conn());
+}
+
+sub errmsg {
+ my $this = shift();
+ return Net::Z3950::ZOOM::connection_errmsg($this->_conn());
+}
+
+sub addinfo {
+ my $this = shift();
+ return Net::Z3950::ZOOM::connection_addinfo($this->_conn());
+}
+
+sub diagset {
+ my $this = shift();
+ return Net::Z3950::ZOOM::connection_diagset($this->_conn());
+}
+
+sub connect {
+ my $this = shift();
+ my($host, $port) = @_;
+
+ $port = 0 if !defined $port;
+ Net::Z3950::ZOOM::connection_connect($this->_conn(), $host, $port);
+ $this->_check();
+ # No return value
+}
+
+sub option {
+ my $this = shift();
+ my($key, $value) = @_;
+
+ my $oldval = Net::Z3950::ZOOM::connection_option_get($this->_conn(), $key);
+ Net::Z3950::ZOOM::connection_option_set($this->_conn(), $key, $value)
+ if defined $value;
+
+ return $oldval;
+}
+
+sub option_binary {
+ my $this = shift();
+ my($key, $value) = @_;
+
+ my $dummylen = 0;
+ my $oldval = Net::Z3950::ZOOM::connection_option_getl($this->_conn(),
+ $key, $dummylen);
+ Net::Z3950::ZOOM::connection_option_setl($this->_conn(), $key,
+ $value, length($value))
+ if defined $value;
+
+ return $oldval;
+}
+
+sub search {
+ my $this = shift();
+ my($query) = @_;
+
+ my $_rs = Net::Z3950::ZOOM::connection_search($this->_conn(),
+ $query->_query());
+ $this->_check();
+ return _new ZOOM::ResultSet($this, $query, $_rs);
+}
+
+sub search_pqf {
+ my $this = shift();
+ my($pqf) = @_;
+
+ my $_rs = Net::Z3950::ZOOM::connection_search_pqf($this->_conn(), $pqf);
+ $this->_check();
+ return _new ZOOM::ResultSet($this, $pqf, $_rs);
+}
+
+sub scan_pqf {
+ my $this = shift();
+ my($startterm) = @_;
+
+ my $_ss = Net::Z3950::ZOOM::connection_scan($this->_conn(), $startterm);
+ $this->_check();
+ return _new ZOOM::ScanSet($this, $startterm, $_ss);
+}
+
+sub scan {
+ my $this = shift();
+ my($query) = @_;
+
+ my $_ss = Net::Z3950::ZOOM::connection_scan1($this->_conn(),
+ $query->_query());
+ $this->_check();
+ return _new ZOOM::ScanSet($this, $query, $_ss);
+}
+
+sub package {
+ my $this = shift();
+ my($options) = @_;
+
+ my $_o = defined $options ? $options->_opts() :
+ Net::Z3950::ZOOM::options_create();
+ my $_p = Net::Z3950::ZOOM::connection_package($this->_conn(), $_o)
+ or ZOOM::_oops(ZOOM::Error::PACKAGE);
+
+ return _new ZOOM::Package($this, $options, $_p);
+}
+
+sub last_event {
+ my $this = shift();
+
+ return Net::Z3950::ZOOM::connection_last_event($this->_conn());
+}
+
+sub is_idle {
+ my $this = shift();
+
+ return Net::Z3950::ZOOM::connection_is_idle($this->_conn());
+}
+
+sub peek_event {
+ my $this = shift();
+
+ return Net::Z3950::ZOOM::connection_peek_event($this->_conn());
+}
+
+sub destroy {
+ my $this = shift();
+
+ Net::Z3950::ZOOM::connection_destroy($this->_conn());
+ $this->{_conn} = undef;
+}
+
+
+# ----------------------------------------------------------------------------
+
+package ZOOM::Query;
+
+sub new {
+ my $class = shift();
+ die "You can't create $class objects: it's a virtual base class";
+}
+
+# PRIVATE to this class and ZOOM::Connection::search()
+sub _query {
+ my $this = shift();
+
+ my $_query = $this->{_query};
+ die "{_query} undefined: has this Query been destroy()ed?"
+ if !defined $_query;
+
+ return $_query;
+}
+
+sub sortby {
+ my $this = shift();
+ my($sortby) = @_;