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>.
23 use C4::Reserves qw( GetReserveStatus );
24 use C4::Search qw( new_record_from_zebra );
31 Koha::Z3950Responder::Session
35 An abstract class where backend-specific session modules are derived from.
36 Z3950Responder creates one of the child classes depending on the SearchEngine
41 This class contains common functions for handling searching for and fetching
42 of records. It can optionally add item status information to the returned
43 records. The backend-specific abstract methods need to be implemented in a
48 OIDs and diagnostic codes used in Z39.50
53 UNIMARC_OID => '1.2.840.10003.5.1',
54 USMARC_OID => '1.2.840.10003.5.10',
55 MARCXML_OID => '1.2.840.10003.5.109.10'
59 ERR_TEMPORARY_ERROR => 2,
60 ERR_PRESENT_OUT_OF_RANGE => 13,
61 ERR_RECORD_TOO_LARGE => 16,
62 ERR_NO_SUCH_RESULTSET => 30,
63 ERR_SEARCH_FAILED => 125,
64 ERR_SYNTAX_UNSUPPORTED => 239,
65 ERR_DB_DOES_NOT_EXIST => 235,
70 =head2 INSTANCE METHODS
74 my $session = $self->new({
75 server => $z3950responder,
84 my ( $class, $args ) = @_;
88 logger => Koha::Logger->get({ interface => 'z3950' }),
92 if ( $self->{server}->{debug} ) {
93 $self->{logger}->debug_to_screen();
96 $self->log_info('connected');
101 =head3 search_handler
103 Callback that is called when a new search is performed
105 Calls C<start_search> for backend-specific retrieval logic
110 my ( $self, $args ) = @_;
112 my $database = $args->{DATABASES}->[0];
114 if ( $database ne $Koha::SearchEngine::BIBLIOS_INDEX && $database ne $Koha::SearchEngine::AUTHORITIES_INDEX ) {
115 $self->set_error( $args, $self->ERR_DB_DOES_NOT_EXIST, 'No such database' );
119 my $query = $args->{QUERY};
120 $self->log_info("received search for '$query', (RS $args->{SETNAME})");
122 my ($resultset, $hits) = $self->start_search( $args, $self->{server}->{num_to_prefetch} );
123 return unless $resultset;
125 $args->{HITS} = $hits;
126 $self->{resultsets}->{ $args->{SETNAME} } = $resultset;
131 Callback that is called when records are requested
133 Calls C<fetch_record> for backend-specific retrieval logic
138 my ( $self, $args ) = @_;
140 $self->log_debug("received fetch for RS $args->{SETNAME}, record $args->{OFFSET}");
142 my $server = $self->{server};
144 my $form_oid = $args->{REQ_FORM} // '';
145 my $composition = $args->{COMP} // '';
146 $self->log_debug(" form OID '$form_oid', composition '$composition'");
148 my $resultset = $self->{resultsets}->{ $args->{SETNAME} };
149 # The offset comes across 1-indexed.
150 my $offset = $args->{OFFSET} - 1;
152 return unless $self->check_fetch( $resultset, $args, $offset, 1 );
154 $args->{LAST} = 1 if ( $offset == $resultset->{hits} - 1 );
156 my $record = $self->fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
157 return unless $record;
159 # Note that new_record_from_zebra is badly named and works also with Elasticsearch
160 $record = C4::Search::new_record_from_zebra(
161 $resultset->{database} eq 'biblios' ? 'biblioserver' : 'authorityserver',
165 if ( $server->{add_item_status_subfield} ) {
166 my $tag = $server->{item_tag};
168 foreach my $field ( $record->field($tag) ) {
169 $self->add_item_status( $field );
173 if ( $form_oid eq $self->MARCXML_OID && $composition eq 'marcxml' ) {
174 $args->{RECORD} = $record->as_xml_record();
175 } elsif ( ( $form_oid eq $self->USMARC_OID || $form_oid eq $self->UNIMARC_OID ) && ( !$composition || $composition eq 'F' ) ) {
176 $args->{RECORD} = $record->as_usmarc();
178 $self->set_error( $args, $self->ERR_SYNTAX_UNSUPPORTED, "Unsupported syntax/composition $form_oid/$composition" );
185 Callback that is called when a session is terminated
190 my ( $self, $args ) = @_;
192 # Override in a child class to add functionality
197 my ($resultset, $hits) = $self->_start_search( $args, $self->{server}->{num_to_prefetch} );
199 A backend-specific method for starting a new search
204 die('Abstract method');
209 $self->check_fetch($resultset, $args, $offset, $num_records);
211 Check that the fetch request parameters are within bounds of the result set.
216 my ( $self, $resultset, $args, $offset, $num_records ) = @_;
218 if ( !defined( $resultset ) ) {
219 $self->set_error( $args, ERR_NO_SUCH_RESULTSET, 'No such resultset' );
223 if ( $offset < 0 || $offset + $num_records > $resultset->{hits} ) {
224 $self->set_error( $args, ERR_PRESENT_OUT_OF_RANGE, 'Present request out of range' );
233 my $record = $self->_fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
235 A backend-specific method for fetching a record
240 die('Abstract method');
243 =head3 add_item_status
245 $self->add_item_status( $field );
247 Add item status to the given field
251 sub add_item_status {
252 my ( $self, $field ) = @_;
254 my $server = $self->{server};
256 my $itemnumber_subfield = $server->{itemnumber_subfield};
257 my $add_subfield = $server->{add_item_status_subfield};
258 my $status_strings = $server->{status_strings};
260 my $itemnumber = $field->subfield($itemnumber_subfield);
261 next unless $itemnumber;
263 my $item = Koha::Items->find( $itemnumber );
268 if ( $item->onloan() ) {
269 push @statuses, $status_strings->{CHECKED_OUT};
272 if ( $item->itemlost() ) {
273 push @statuses, $status_strings->{LOST};
276 if ( $item->notforloan() ) {
277 push @statuses, $status_strings->{NOT_FOR_LOAN};
280 if ( $item->damaged() ) {
281 push @statuses, $status_strings->{DAMAGED};
284 if ( $item->withdrawn() ) {
285 push @statuses, $status_strings->{WITHDRAWN};
288 if ( my $transfer = $item->get_transfer ) {
289 push @statuses, $status_strings->{IN_TRANSIT} if $transfer->in_transit;
292 if ( GetReserveStatus( $itemnumber ) ne '' ) {
293 push @statuses, $status_strings->{ON_HOLD};
296 if ( $server->{add_status_multi_subfield} ) {
297 $field->add_subfields( map { ( $add_subfield, $_ ) } ( @statuses ? @statuses : $status_strings->{AVAILABLE} ) );
299 $field->add_subfields( $add_subfield, @statuses ? join( ', ', @statuses ) : $status_strings->{AVAILABLE} );
306 $self->log_debug('Message');
308 Output a debug message
313 my ( $self, $msg ) = @_;
314 $self->{logger}->debug("[$self->{peer}] $msg");
319 $self->log_info('Message');
321 Output an info message
326 my ( $self, $msg ) = @_;
327 $self->{logger}->info("[$self->{peer}] $msg");
332 $self->log_error('Message');
334 Output an error message
339 my ( $self, $msg ) = @_;
340 $self->{logger}->error("[$self->{peer}] $msg");
345 $self->set_error($args, $self->ERR_SEARCH_FAILED, 'Backend connection failed' );
347 Set and log an error code and diagnostic message to be returned to the client
352 my ( $self, $args, $code, $msg ) = @_;
354 ( $args->{ERR_CODE}, $args->{ERR_STR} ) = ( $code, $msg );
356 $self->log_error(" returning error $code: $msg");