1 package Koha::Z3950Responder::Session;
3 # Copyright ByWater Solutions 2016
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use C4::Circulation qw( GetTransfers );
24 use C4::Reserves qw( GetReserveStatus );
25 use C4::Search qw( new_record_from_zebra );
32 Koha::Z3950Responder::Session
36 An abstract class where backend-specific session modules are derived from.
37 Z3950Responder creates one of the child classes depending on the SearchEngine
42 This class contains common functions for handling searching for and fetching
43 of records. It can optionally add item status information to the returned
44 records. The backend-specific abstract methods need to be implemented in a
49 OIDs and diagnostic codes used in Z39.50
54 UNIMARC_OID => '1.2.840.10003.5.1',
55 USMARC_OID => '1.2.840.10003.5.10',
56 MARCXML_OID => '1.2.840.10003.5.109.10'
60 ERR_TEMPORARY_ERROR => 2,
61 ERR_PRESENT_OUT_OF_RANGE => 13,
62 ERR_RECORD_TOO_LARGE => 16,
63 ERR_NO_SUCH_RESULTSET => 30,
64 ERR_SEARCH_FAILED => 125,
65 ERR_SYNTAX_UNSUPPORTED => 239,
66 ERR_DB_DOES_NOT_EXIST => 235,
71 =head2 INSTANCE METHODS
75 my $session = $self->new({
76 server => $z3950responder,
85 my ( $class, $args ) = @_;
89 logger => Koha::Logger->get({ interface => 'z3950' }),
93 if ( $self->{server}->{debug} ) {
94 $self->{logger}->debug_to_screen();
97 $self->log_info('connected');
102 =head3 search_handler
104 Callback that is called when a new search is performed
106 Calls C<start_search> for backend-specific retrieval logic
111 my ( $self, $args ) = @_;
113 my $database = $args->{DATABASES}->[0];
115 if ( $database ne $Koha::SearchEngine::BIBLIOS_INDEX && $database ne $Koha::SearchEngine::AUTHORITIES_INDEX ) {
116 $self->set_error( $args, $self->ERR_DB_DOES_NOT_EXIST, 'No such database' );
120 my $query = $args->{QUERY};
121 $self->log_info("received search for '$query', (RS $args->{SETNAME})");
123 my ($resultset, $hits) = $self->start_search( $args, $self->{server}->{num_to_prefetch} );
124 return unless $resultset;
126 $args->{HITS} = $hits;
127 $self->{resultsets}->{ $args->{SETNAME} } = $resultset;
132 Callback that is called when records are requested
134 Calls C<fetch_record> for backend-specific retrieval logic
139 my ( $self, $args ) = @_;
141 $self->log_debug("received fetch for RS $args->{SETNAME}, record $args->{OFFSET}");
143 my $server = $self->{server};
145 my $form_oid = $args->{REQ_FORM} // '';
146 my $composition = $args->{COMP} // '';
147 $self->log_debug(" form OID '$form_oid', composition '$composition'");
149 my $resultset = $self->{resultsets}->{ $args->{SETNAME} };
150 # The offset comes across 1-indexed.
151 my $offset = $args->{OFFSET} - 1;
153 return unless $self->check_fetch( $resultset, $args, $offset, 1 );
155 $args->{LAST} = 1 if ( $offset == $resultset->{hits} - 1 );
157 my $record = $self->fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
158 return unless $record;
160 # Note that new_record_from_zebra is badly named and works also with Elasticsearch
161 $record = C4::Search::new_record_from_zebra(
162 $resultset->{database} eq 'biblios' ? 'biblioserver' : 'authorityserver',
166 if ( $server->{add_item_status_subfield} ) {
167 my $tag = $server->{item_tag};
169 foreach my $field ( $record->field($tag) ) {
170 $self->add_item_status( $field );
174 if ( $form_oid eq $self->MARCXML_OID && $composition eq 'marcxml' ) {
175 $args->{RECORD} = $record->as_xml_record();
176 } elsif ( ( $form_oid eq $self->USMARC_OID || $form_oid eq $self->UNIMARC_OID ) && ( !$composition || $composition eq 'F' ) ) {
177 $args->{RECORD} = $record->as_usmarc();
179 $self->set_error( $args, $self->ERR_SYNTAX_UNSUPPORTED, "Unsupported syntax/composition $form_oid/$composition" );
186 Callback that is called when a session is terminated
191 my ( $self, $args ) = @_;
193 # Override in a child class to add functionality
198 my ($resultset, $hits) = $self->_start_search( $args, $self->{server}->{num_to_prefetch} );
200 A backend-specific method for starting a new search
205 die('Abstract method');
210 $self->check_fetch($resultset, $args, $offset, $num_records);
212 Check that the fetch request parameters are within bounds of the result set.
217 my ( $self, $resultset, $args, $offset, $num_records ) = @_;
219 if ( !defined( $resultset ) ) {
220 $self->set_error( $args, ERR_NO_SUCH_RESULTSET, 'No such resultset' );
224 if ( $offset < 0 || $offset + $num_records > $resultset->{hits} ) {
225 $self->set_error( $args, ERR_PRESENT_OUT_OF_RANGE, 'Present request out of range' );
234 my $record = $self->_fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
236 A backend-specific method for fetching a record
241 die('Abstract method');
244 =head3 add_item_status
246 $self->add_item_status( $field );
248 Add item status to the given field
252 sub add_item_status {
253 my ( $self, $field ) = @_;
255 my $server = $self->{server};
257 my $itemnumber_subfield = $server->{itemnumber_subfield};
258 my $add_subfield = $server->{add_item_status_subfield};
259 my $status_strings = $server->{status_strings};
261 my $itemnumber = $field->subfield($itemnumber_subfield);
262 next unless $itemnumber;
264 my $item = Koha::Items->find( $itemnumber );
269 if ( $item->onloan() ) {
270 push @statuses, $status_strings->{CHECKED_OUT};
273 if ( $item->itemlost() ) {
274 push @statuses, $status_strings->{LOST};
277 if ( $item->notforloan() ) {
278 push @statuses, $status_strings->{NOT_FOR_LOAN};
281 if ( $item->damaged() ) {
282 push @statuses, $status_strings->{DAMAGED};
285 if ( $item->withdrawn() ) {
286 push @statuses, $status_strings->{WITHDRAWN};
289 if ( scalar( GetTransfers( $itemnumber ) ) ) {
290 push @statuses, $status_strings->{IN_TRANSIT};
293 if ( GetReserveStatus( $itemnumber ) ne '' ) {
294 push @statuses, $status_strings->{ON_HOLD};
297 if ( $server->{add_status_multi_subfield} ) {
298 $field->add_subfields( map { ( $add_subfield, $_ ) } ( @statuses ? @statuses : $status_strings->{AVAILABLE} ) );
300 $field->add_subfields( $add_subfield, @statuses ? join( ', ', @statuses ) : $status_strings->{AVAILABLE} );
307 $self->log_debug('Message');
309 Output a debug message
314 my ( $self, $msg ) = @_;
315 $self->{logger}->debug("[$self->{peer}] $msg");
320 $self->log_info('Message');
322 Output an info message
327 my ( $self, $msg ) = @_;
328 $self->{logger}->info("[$self->{peer}] $msg");
333 $self->log_error('Message');
335 Output an error message
340 my ( $self, $msg ) = @_;
341 $self->{logger}->error("[$self->{peer}] $msg");
346 $self->set_error($args, $self->ERR_SEARCH_FAILED, 'Backend connection failed' );
348 Set and log an error code and diagnostic message to be returned to the client
353 my ( $self, $args, $code, $msg ) = @_;
355 ( $args->{ERR_CODE}, $args->{ERR_STR} ) = ( $code, $msg );
357 $self->log_error(" returning error $code: $msg");