Main Koha release repository
https://koha-community.org
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
8.5 KiB
362 lines
8.5 KiB
package Koha::Z3950Responder::Session;
|
|
|
|
# Copyright ByWater Solutions 2016
|
|
#
|
|
# This file is part of Koha.
|
|
#
|
|
# Koha is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Koha is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Koha; if not, see <http://www.gnu.org/licenses>.
|
|
|
|
use Modern::Perl;
|
|
|
|
use C4::Circulation qw( GetTransfers );
|
|
use C4::Context;
|
|
use C4::Reserves qw( GetReserveStatus );
|
|
use C4::Search qw();
|
|
|
|
use Koha::Items;
|
|
use Koha::Logger;
|
|
|
|
=head1 NAME
|
|
|
|
Koha::Z3950Responder::Session
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
An abstract class where backend-specific session modules are derived from.
|
|
Z3950Responder creates one of the child classes depending on the SearchEngine
|
|
preference.
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This class contains common functions for handling searching for and fetching
|
|
of records. It can optionally add item status information to the returned
|
|
records. The backend-specific abstract methods need to be implemented in a
|
|
child class.
|
|
|
|
=head2 CONSTANTS
|
|
|
|
OIDs and diagnostic codes used in Z39.50
|
|
|
|
=cut
|
|
|
|
use constant {
|
|
UNIMARC_OID => '1.2.840.10003.5.1',
|
|
USMARC_OID => '1.2.840.10003.5.10',
|
|
MARCXML_OID => '1.2.840.10003.5.109.10'
|
|
};
|
|
|
|
use constant {
|
|
ERR_TEMPORARY_ERROR => 2,
|
|
ERR_PRESENT_OUT_OF_RANGE => 13,
|
|
ERR_RECORD_TOO_LARGE => 16,
|
|
ERR_NO_SUCH_RESULTSET => 30,
|
|
ERR_SEARCH_FAILED => 125,
|
|
ERR_SYNTAX_UNSUPPORTED => 239,
|
|
ERR_DB_DOES_NOT_EXIST => 235,
|
|
};
|
|
|
|
=head1 FUNCTIONS
|
|
|
|
=head2 INSTANCE METHODS
|
|
|
|
=head3 new
|
|
|
|
my $session = $self->new({
|
|
server => $z3950responder,
|
|
peer => 'PEER NAME'
|
|
});
|
|
|
|
Instantiate a Session
|
|
|
|
=cut
|
|
|
|
sub new {
|
|
my ( $class, $args ) = @_;
|
|
|
|
my $self = bless( {
|
|
%$args,
|
|
logger => Koha::Logger->get({ interface => 'z3950' }),
|
|
resultsets => {},
|
|
}, $class );
|
|
|
|
if ( $self->{server}->{debug} ) {
|
|
$self->{logger}->debug_to_screen();
|
|
}
|
|
|
|
$self->log_info('connected');
|
|
|
|
return $self;
|
|
}
|
|
|
|
=head3 search_handler
|
|
|
|
Callback that is called when a new search is performed
|
|
|
|
Calls C<start_search> for backend-specific retrieval logic
|
|
|
|
=cut
|
|
|
|
sub search_handler {
|
|
my ( $self, $args ) = @_;
|
|
|
|
my $database = $args->{DATABASES}->[0];
|
|
|
|
if ( $database ne $Koha::SearchEngine::BIBLIOS_INDEX && $database ne $Koha::SearchEngine::AUTHORITIES_INDEX ) {
|
|
$self->set_error( $args, $self->ERR_DB_DOES_NOT_EXIST, 'No such database' );
|
|
return;
|
|
}
|
|
|
|
my $query = $args->{QUERY};
|
|
$self->log_info("received search for '$query', (RS $args->{SETNAME})");
|
|
|
|
my ($resultset, $hits) = $self->start_search( $args, $self->{server}->{num_to_prefetch} );
|
|
return unless $resultset;
|
|
|
|
$args->{HITS} = $hits;
|
|
$self->{resultsets}->{ $args->{SETNAME} } = $resultset;
|
|
}
|
|
|
|
=head3 fetch_handler
|
|
|
|
Callback that is called when records are requested
|
|
|
|
Calls C<fetch_record> for backend-specific retrieval logic
|
|
|
|
=cut
|
|
|
|
sub fetch_handler {
|
|
my ( $self, $args ) = @_;
|
|
|
|
$self->log_debug("received fetch for RS $args->{SETNAME}, record $args->{OFFSET}");
|
|
|
|
my $server = $self->{server};
|
|
|
|
my $form_oid = $args->{REQ_FORM} // '';
|
|
my $composition = $args->{COMP} // '';
|
|
$self->log_debug(" form OID '$form_oid', composition '$composition'");
|
|
|
|
my $resultset = $self->{resultsets}->{ $args->{SETNAME} };
|
|
# The offset comes across 1-indexed.
|
|
my $offset = $args->{OFFSET} - 1;
|
|
|
|
return unless $self->check_fetch( $resultset, $args, $offset, 1 );
|
|
|
|
$args->{LAST} = 1 if ( $offset == $resultset->{hits} - 1 );
|
|
|
|
my $record = $self->fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
|
|
return unless $record;
|
|
|
|
# Note that new_record_from_zebra is badly named and works also with Elasticsearch
|
|
$record = C4::Search::new_record_from_zebra(
|
|
$resultset->{database} eq 'biblios' ? 'biblioserver' : 'authorityserver',
|
|
$record
|
|
);
|
|
|
|
if ( $server->{add_item_status_subfield} ) {
|
|
my $tag = $server->{item_tag};
|
|
|
|
foreach my $field ( $record->field($tag) ) {
|
|
$self->add_item_status( $field );
|
|
}
|
|
}
|
|
|
|
if ( $form_oid eq $self->MARCXML_OID && $composition eq 'marcxml' ) {
|
|
$args->{RECORD} = $record->as_xml_record();
|
|
} elsif ( ( $form_oid eq $self->USMARC_OID || $form_oid eq $self->UNIMARC_OID ) && ( !$composition || $composition eq 'F' ) ) {
|
|
$args->{RECORD} = $record->as_usmarc();
|
|
} else {
|
|
$self->set_error( $args, $self->ERR_SYNTAX_UNSUPPORTED, "Unsupported syntax/composition $form_oid/$composition" );
|
|
return;
|
|
}
|
|
}
|
|
|
|
=head3 close_handler
|
|
|
|
Callback that is called when a session is terminated
|
|
|
|
=cut
|
|
|
|
sub close_handler {
|
|
my ( $self, $args ) = @_;
|
|
|
|
# Override in a child class to add functionality
|
|
}
|
|
|
|
=head3 start_search
|
|
|
|
my ($resultset, $hits) = $self->_start_search( $args, $self->{server}->{num_to_prefetch} );
|
|
|
|
A backend-specific method for starting a new search
|
|
|
|
=cut
|
|
|
|
sub start_search {
|
|
die('Abstract method');
|
|
}
|
|
|
|
=head3 check_fetch
|
|
|
|
$self->check_fetch($resultset, $args, $offset, $num_records);
|
|
|
|
Check that the fetch request parameters are within bounds of the result set.
|
|
|
|
=cut
|
|
|
|
sub check_fetch {
|
|
my ( $self, $resultset, $args, $offset, $num_records ) = @_;
|
|
|
|
if ( !defined( $resultset ) ) {
|
|
$self->set_error( $args, ERR_NO_SUCH_RESULTSET, 'No such resultset' );
|
|
return 0;
|
|
}
|
|
|
|
if ( $offset < 0 || $offset + $num_records > $resultset->{hits} ) {
|
|
$self->set_error( $args, ERR_PRESENT_OUT_OF_RANGE, 'Present request out of range' );
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
=head3 fetch_record
|
|
|
|
my $record = $self->_fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
|
|
|
|
A backend-specific method for fetching a record
|
|
|
|
=cut
|
|
|
|
sub fetch_record {
|
|
die('Abstract method');
|
|
}
|
|
|
|
=head3 add_item_status
|
|
|
|
$self->add_item_status( $field );
|
|
|
|
Add item status to the given field
|
|
|
|
=cut
|
|
|
|
sub add_item_status {
|
|
my ( $self, $field ) = @_;
|
|
|
|
my $server = $self->{server};
|
|
|
|
my $itemnumber_subfield = $server->{itemnumber_subfield};
|
|
my $add_subfield = $server->{add_item_status_subfield};
|
|
my $status_strings = $server->{status_strings};
|
|
|
|
my $itemnumber = $field->subfield($itemnumber_subfield);
|
|
next unless $itemnumber;
|
|
|
|
my $item = Koha::Items->find( $itemnumber );
|
|
return unless $item;
|
|
|
|
my @statuses;
|
|
|
|
if ( $item->onloan() ) {
|
|
push @statuses, $status_strings->{CHECKED_OUT};
|
|
}
|
|
|
|
if ( $item->itemlost() ) {
|
|
push @statuses, $status_strings->{LOST};
|
|
}
|
|
|
|
if ( $item->notforloan() ) {
|
|
push @statuses, $status_strings->{NOT_FOR_LOAN};
|
|
}
|
|
|
|
if ( $item->damaged() ) {
|
|
push @statuses, $status_strings->{DAMAGED};
|
|
}
|
|
|
|
if ( $item->withdrawn() ) {
|
|
push @statuses, $status_strings->{WITHDRAWN};
|
|
}
|
|
|
|
if ( scalar( GetTransfers( $itemnumber ) ) ) {
|
|
push @statuses, $status_strings->{IN_TRANSIT};
|
|
}
|
|
|
|
if ( GetReserveStatus( $itemnumber ) ne '' ) {
|
|
push @statuses, $status_strings->{ON_HOLD};
|
|
}
|
|
|
|
$field->delete_subfield( code => $itemnumber_subfield );
|
|
|
|
if ( $server->{add_status_multi_subfield} ) {
|
|
$field->add_subfields( map { ( $add_subfield, $_ ) } ( @statuses ? @statuses : $status_strings->{AVAILABLE} ) );
|
|
} else {
|
|
$field->add_subfields( $add_subfield, @statuses ? join( ', ', @statuses ) : $status_strings->{AVAILABLE} );
|
|
}
|
|
}
|
|
|
|
|
|
=head3 log_debug
|
|
|
|
$self->log_debug('Message');
|
|
|
|
Output a debug message
|
|
|
|
=cut
|
|
|
|
sub log_debug {
|
|
my ( $self, $msg ) = @_;
|
|
$self->{logger}->debug("[$self->{peer}] $msg");
|
|
}
|
|
|
|
=head3 log_info
|
|
|
|
$self->log_info('Message');
|
|
|
|
Output an info message
|
|
|
|
=cut
|
|
|
|
sub log_info {
|
|
my ( $self, $msg ) = @_;
|
|
$self->{logger}->info("[$self->{peer}] $msg");
|
|
}
|
|
|
|
=head3 log_error
|
|
|
|
$self->log_error('Message');
|
|
|
|
Output an error message
|
|
|
|
=cut
|
|
|
|
sub log_error {
|
|
my ( $self, $msg ) = @_;
|
|
$self->{logger}->error("[$self->{peer}] $msg");
|
|
}
|
|
|
|
=head3 set_error
|
|
|
|
$self->set_error($args, $self->ERR_SEARCH_FAILED, 'Backend connection failed' );
|
|
|
|
Set and log an error code and diagnostic message to be returned to the client
|
|
|
|
=cut
|
|
|
|
sub set_error {
|
|
my ( $self, $args, $code, $msg ) = @_;
|
|
|
|
( $args->{ERR_CODE}, $args->{ERR_STR} ) = ( $code, $msg );
|
|
|
|
$self->log_error(" returning error $code: $msg");
|
|
}
|
|
|
|
1;
|
|
|