Bug 22630: Allow to change homebranch in course reserves
[koha.git] / Koha / Z3950Responder / Session.pm
blob64c866dfaf36e364c2674f7e82a2e1de0a7787a1
1 #!/usr/bin/perl
3 package Koha::Z3950Responder::Session;
5 # Copyright ByWater Solutions 2016
7 # This file is part of Koha.
9 # Koha is free software; you can redistribute it and/or modify it
10 # under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # Koha is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Modern::Perl;
24 use C4::Circulation qw( GetTransfers );
25 use C4::Context;
26 use C4::Reserves qw( GetReserveStatus );
27 use C4::Search qw();
29 use Koha::Items;
30 use Koha::Logger;
32 =head1 NAME
34 Koha::Z3950Responder::Session
36 =head1 SYNOPSIS
38 An abstract class where backend-specific session modules are derived from.
39 Z3950Responder creates one of the child classes depending on the SearchEngine
40 preference.
42 =head1 DESCRIPTION
44 This class contains common functions for handling searching for and fetching
45 of records. It can optionally add item status information to the returned
46 records. The backend-specific abstract methods need to be implemented in a
47 child class.
49 =head2 CONSTANTS
51 OIDs and diagnostic codes used in Z39.50
53 =cut
55 use constant {
56 UNIMARC_OID => '1.2.840.10003.5.1',
57 USMARC_OID => '1.2.840.10003.5.10',
58 MARCXML_OID => '1.2.840.10003.5.109.10'
61 use constant {
62 ERR_TEMPORARY_ERROR => 2,
63 ERR_PRESENT_OUT_OF_RANGE => 13,
64 ERR_RECORD_TOO_LARGE => 16,
65 ERR_NO_SUCH_RESULTSET => 30,
66 ERR_SEARCH_FAILED => 125,
67 ERR_SYNTAX_UNSUPPORTED => 239,
68 ERR_DB_DOES_NOT_EXIST => 235,
71 =head1 FUNCTIONS
73 =head2 INSTANCE METHODS
75 =head3 new
77 my $session = $self->new({
78 server => $z3950responder,
79 peer => 'PEER NAME'
80 });
82 Instantiate a Session
84 =cut
86 sub new {
87 my ( $class, $args ) = @_;
89 my $self = bless( {
90 %$args,
91 logger => Koha::Logger->get({ interface => 'z3950' }),
92 resultsets => {},
93 }, $class );
95 if ( $self->{server}->{debug} ) {
96 $self->{logger}->debug_to_screen();
99 $self->log_info('connected');
101 return $self;
104 =head3 search_handler
106 Callback that is called when a new search is performed
108 Calls C<start_search> for backend-specific retrieval logic
110 =cut
112 sub search_handler {
113 my ( $self, $args ) = @_;
115 my $database = $args->{DATABASES}->[0];
117 if ( $database ne $Koha::SearchEngine::BIBLIOS_INDEX && $database ne $Koha::SearchEngine::AUTHORITIES_INDEX ) {
118 $self->set_error( $args, $self->ERR_DB_DOES_NOT_EXIST, 'No such database' );
119 return;
122 my $query = $args->{QUERY};
123 $self->log_info("received search for '$query', (RS $args->{SETNAME})");
125 my ($resultset, $hits) = $self->start_search( $args, $self->{server}->{num_to_prefetch} );
126 return unless $resultset;
128 $args->{HITS} = $hits;
129 $self->{resultsets}->{ $args->{SETNAME} } = $resultset;
132 =head3 fetch_handler
134 Callback that is called when records are requested
136 Calls C<fetch_record> for backend-specific retrieval logic
138 =cut
140 sub fetch_handler {
141 my ( $self, $args ) = @_;
143 $self->log_debug("received fetch for RS $args->{SETNAME}, record $args->{OFFSET}");
145 my $server = $self->{server};
147 my $form_oid = $args->{REQ_FORM} // '';
148 my $composition = $args->{COMP} // '';
149 $self->log_debug(" form OID '$form_oid', composition '$composition'");
151 my $resultset = $self->{resultsets}->{ $args->{SETNAME} };
152 # The offset comes across 1-indexed.
153 my $offset = $args->{OFFSET} - 1;
155 return unless $self->check_fetch( $resultset, $args, $offset, 1 );
157 $args->{LAST} = 1 if ( $offset == $resultset->{hits} - 1 );
159 my $record = $self->fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
160 return unless $record;
162 # Note that new_record_from_zebra is badly named and works also with Elasticsearch
163 $record = C4::Search::new_record_from_zebra(
164 $resultset->{database} eq 'biblios' ? 'biblioserver' : 'authorityserver',
165 $record
168 if ( $server->{add_item_status_subfield} ) {
169 my $tag = $server->{item_tag};
171 foreach my $field ( $record->field($tag) ) {
172 $self->add_item_status( $field );
176 if ( $form_oid eq $self->MARCXML_OID && $composition eq 'marcxml' ) {
177 $args->{RECORD} = $record->as_xml_record();
178 } elsif ( ( $form_oid eq $self->USMARC_OID || $form_oid eq $self->UNIMARC_OID ) && ( !$composition || $composition eq 'F' ) ) {
179 $args->{RECORD} = $record->as_usmarc();
180 } else {
181 $self->set_error( $args, $self->ERR_SYNTAX_UNSUPPORTED, "Unsupported syntax/composition $form_oid/$composition" );
182 return;
186 =head3 close_handler
188 Callback that is called when a session is terminated
190 =cut
192 sub close_handler {
193 my ( $self, $args ) = @_;
195 # Override in a child class to add functionality
198 =head3 start_search
200 my ($resultset, $hits) = $self->_start_search( $args, $self->{server}->{num_to_prefetch} );
202 A backend-specific method for starting a new search
204 =cut
206 sub start_search {
207 die('Abstract method');
210 =head3 check_fetch
212 $self->check_fetch($resultset, $args, $offset, $num_records);
214 Check that the fetch request parameters are within bounds of the result set.
216 =cut
218 sub check_fetch {
219 my ( $self, $resultset, $args, $offset, $num_records ) = @_;
221 if ( !defined( $resultset ) ) {
222 $self->set_error( $args, ERR_NO_SUCH_RESULTSET, 'No such resultset' );
223 return 0;
226 if ( $offset < 0 || $offset + $num_records > $resultset->{hits} ) {
227 $self->set_error( $args, ERR_PRESENT_OUT_OF_RANGE, 'Present request out of range' );
228 return 0;
231 return 1;
234 =head3 fetch_record
236 my $record = $self->_fetch_record( $resultset, $args, $offset, $server->{num_to_prefetch} );
238 A backend-specific method for fetching a record
240 =cut
242 sub fetch_record {
243 die('Abstract method');
246 =head3 add_item_status
248 $self->add_item_status( $field );
250 Add item status to the given field
252 =cut
254 sub add_item_status {
255 my ( $self, $field ) = @_;
257 my $server = $self->{server};
259 my $itemnumber_subfield = $server->{itemnumber_subfield};
260 my $add_subfield = $server->{add_item_status_subfield};
261 my $status_strings = $server->{status_strings};
263 my $itemnumber = $field->subfield($itemnumber_subfield);
264 next unless $itemnumber;
266 my $item = Koha::Items->find( $itemnumber );
267 return unless $item;
269 my @statuses;
271 if ( $item->onloan() ) {
272 push @statuses, $status_strings->{CHECKED_OUT};
275 if ( $item->itemlost() ) {
276 push @statuses, $status_strings->{LOST};
279 if ( $item->notforloan() ) {
280 push @statuses, $status_strings->{NOT_FOR_LOAN};
283 if ( $item->damaged() ) {
284 push @statuses, $status_strings->{DAMAGED};
287 if ( $item->withdrawn() ) {
288 push @statuses, $status_strings->{WITHDRAWN};
291 if ( scalar( GetTransfers( $itemnumber ) ) ) {
292 push @statuses, $status_strings->{IN_TRANSIT};
295 if ( GetReserveStatus( $itemnumber ) ne '' ) {
296 push @statuses, $status_strings->{ON_HOLD};
299 $field->delete_subfield( code => $itemnumber_subfield );
301 if ( $server->{add_status_multi_subfield} ) {
302 $field->add_subfields( map { ( $add_subfield, $_ ) } ( @statuses ? @statuses : $status_strings->{AVAILABLE} ) );
303 } else {
304 $field->add_subfields( $add_subfield, @statuses ? join( ', ', @statuses ) : $status_strings->{AVAILABLE} );
309 =head3 log_debug
311 $self->log_debug('Message');
313 Output a debug message
315 =cut
317 sub log_debug {
318 my ( $self, $msg ) = @_;
319 $self->{logger}->debug("[$self->{peer}] $msg");
322 =head3 log_info
324 $self->log_info('Message');
326 Output an info message
328 =cut
330 sub log_info {
331 my ( $self, $msg ) = @_;
332 $self->{logger}->info("[$self->{peer}] $msg");
335 =head3 log_error
337 $self->log_error('Message');
339 Output an error message
341 =cut
343 sub log_error {
344 my ( $self, $msg ) = @_;
345 $self->{logger}->error("[$self->{peer}] $msg");
348 =head3 set_error
350 $self->set_error($args, $self->ERR_SEARCH_FAILED, 'Backend connection failed' );
352 Set and log an error code and diagnostic message to be returned to the client
354 =cut
356 sub set_error {
357 my ( $self, $args, $code, $msg ) = @_;
359 ( $args->{ERR_CODE}, $args->{ERR_STR} ) = ( $code, $msg );
361 $self->log_error(" returning error $code: $msg");