Bug 16169: Change prototype for C4::Biblio::TransformMarcToKoha
[koha.git] / C4 / Search.pm
1 package C4::Search;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use strict;
19 #use warnings; FIXME - Bug 2505
20 require Exporter;
21 use C4::Context;
22 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha;      # getFacets
24 use Koha::DateUtils;
25 use Lingua::Stem;
26 use C4::Search::PazPar2;
27 use XML::Simple;
28 use C4::Members qw(GetHideLostItemsPreference);
29 use C4::XSLT;
30 use C4::Branch;
31 use C4::Reserves;    # GetReserveStatus
32 use C4::Debug;
33 use C4::Charset;
34 use Koha::Libraries;
35 use YAML;
36 use URI::Escape;
37 use Business::ISBN;
38 use MARC::Record;
39 use MARC::Field;
40 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
41
42 BEGIN {
43     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
44 }
45
46 =head1 NAME
47
48 C4::Search - Functions for searching the Koha catalog.
49
50 =head1 SYNOPSIS
51
52 See opac/opac-search.pl or catalogue/search.pl for example of usage
53
54 =head1 DESCRIPTION
55
56 This module provides searching functions for Koha's bibliographic databases
57
58 =head1 FUNCTIONS
59
60 =cut
61
62 @ISA    = qw(Exporter);
63 @EXPORT = qw(
64   &FindDuplicate
65   &SimpleSearch
66   &searchResults
67   &getRecords
68   &buildQuery
69   &GetDistinctValues
70   &enabled_staff_search_views
71 );
72
73 # make all your functions, whether exported or not;
74
75 =head2 FindDuplicate
76
77 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
78
79 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
80
81 =cut
82
83 sub FindDuplicate {
84     my ($record) = @_;
85     my $dbh = C4::Context->dbh;
86     my $result = TransformMarcToKoha( $record, '' );
87     my $sth;
88     my $query;
89     my $search;
90     my $type;
91     my ( $biblionumber, $title );
92
93     # search duplicate on ISBN, easy and fast..
94     # ... normalize first
95     if ( $result->{isbn} ) {
96         $result->{isbn} =~ s/\(.*$//;
97         $result->{isbn} =~ s/\s+$//;
98         $query = "isbn:$result->{isbn}";
99     }
100     else {
101         my $QParser;
102         $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
103         my $titleindex;
104         my $authorindex;
105         my $op;
106
107         if ($QParser) {
108             $titleindex = 'title|exact';
109             $authorindex = 'author|exact';
110             $op = '&&';
111             $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
112         } else {
113             $titleindex = 'ti,ext';
114             $authorindex = 'au,ext';
115             $op = 'and';
116         }
117
118         $result->{title} =~ s /\\//g;
119         $result->{title} =~ s /\"//g;
120         $result->{title} =~ s /\(//g;
121         $result->{title} =~ s /\)//g;
122
123         # FIXME: instead of removing operators, could just do
124         # quotes around the value
125         $result->{title} =~ s/(and|or|not)//g;
126         $query = "$titleindex:\"$result->{title}\"";
127         if   ( $result->{author} ) {
128             $result->{author} =~ s /\\//g;
129             $result->{author} =~ s /\"//g;
130             $result->{author} =~ s /\(//g;
131             $result->{author} =~ s /\)//g;
132
133             # remove valid operators
134             $result->{author} =~ s/(and|or|not)//g;
135             $query .= " $op $authorindex:\"$result->{author}\"";
136         }
137     }
138
139     my ( $error, $searchresults, undef ) = SimpleSearch($query); # FIXME :: hardcoded !
140     my @results;
141     if (!defined $error) {
142         foreach my $possible_duplicate_record (@{$searchresults}) {
143             my $marcrecord = new_record_from_zebra(
144                 'biblioserver',
145                 $possible_duplicate_record
146             );
147
148             my $result = TransformMarcToKoha( $marcrecord, '' );
149
150             # FIXME :: why 2 $biblionumber ?
151             if ($result) {
152                 push @results, $result->{'biblionumber'};
153                 push @results, $result->{'title'};
154             }
155         }
156     }
157     return @results;
158 }
159
160 =head2 SimpleSearch
161
162 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
163
164 This function provides a simple search API on the bibliographic catalog
165
166 =over 2
167
168 =item C<input arg:>
169
170     * $query can be a simple keyword or a complete CCL query
171     * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
172     * $offset - If present, represents the number of records at the beginning to omit. Defaults to 0
173     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
174
175
176 =item C<Return:>
177
178     Returns an array consisting of three elements
179     * $error is undefined unless an error is detected
180     * $results is a reference to an array of records.
181     * $total_hits is the number of hits that would have been returned with no limit
182
183     If an error is returned the two other return elements are undefined. If error itself is undefined
184     the other two elements are always defined
185
186 =item C<usage in the script:>
187
188 =back
189
190 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
191
192 if (defined $error) {
193     $template->param(query_error => $error);
194     warn "error: ".$error;
195     output_html_with_http_headers $input, $cookie, $template->output;
196     exit;
197 }
198
199 my $hits = @{$marcresults};
200 my @results;
201
202 for my $r ( @{$marcresults} ) {
203     my $marcrecord = MARC::File::USMARC::decode($r);
204     my $biblio = TransformMarcToKoha($marcrecord,q{});
205
206     #build the iarray of hashs for the template.
207     push @results, {
208         title           => $biblio->{'title'},
209         subtitle        => $biblio->{'subtitle'},
210         biblionumber    => $biblio->{'biblionumber'},
211         author          => $biblio->{'author'},
212         publishercode   => $biblio->{'publishercode'},
213         publicationyear => $biblio->{'publicationyear'},
214         };
215
216 }
217
218 $template->param(result=>\@results);
219
220 =cut
221
222 sub SimpleSearch {
223     my ( $query, $offset, $max_results, $servers )  = @_;
224
225     return ( 'No query entered', undef, undef ) unless $query;
226     # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
227     my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
228     my @zoom_queries;
229     my @tmpresults;
230     my @zconns;
231     my $results = [];
232     my $total_hits = 0;
233
234     my $QParser;
235     $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') && ! ($query =~ m/\w,\w|\w=\w/));
236     if ($QParser) {
237         $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
238     }
239
240     # Initialize & Search Zebra
241     for ( my $i = 0 ; $i < @servers ; $i++ ) {
242         eval {
243             $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
244             if ($QParser) {
245                 $query =~ s/=/:/g;
246                 $QParser->parse( $query );
247                 $query = $QParser->target_syntax($servers[$i]);
248                 $zoom_queries[$i] = new ZOOM::Query::PQF( $query, $zconns[$i]);
249             } else {
250                 $query =~ s/:/=/g;
251                 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
252             }
253             $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
254
255             # error handling
256             my $error =
257                 $zconns[$i]->errmsg() . " ("
258               . $zconns[$i]->errcode() . ") "
259               . $zconns[$i]->addinfo() . " "
260               . $zconns[$i]->diagset();
261
262             return ( $error, undef, undef ) if $zconns[$i]->errcode();
263         };
264         if ($@) {
265
266             # caught a ZOOM::Exception
267             my $error =
268                 $@->message() . " ("
269               . $@->code() . ") "
270               . $@->addinfo() . " "
271               . $@->diagset();
272             warn $error." for query: $query";
273             return ( $error, undef, undef );
274         }
275     }
276
277     _ZOOM_event_loop(
278         \@zconns,
279         \@tmpresults,
280         sub {
281             my ($i, $size) = @_;
282             my $first_record = defined($offset) ? $offset + 1 : 1;
283             my $hits = $tmpresults[ $i - 1 ]->size();
284             $total_hits += $hits;
285             my $last_record = $hits;
286             if ( defined $max_results && $offset + $max_results < $hits ) {
287                 $last_record = $offset + $max_results;
288             }
289
290             for my $j ( $first_record .. $last_record ) {
291                 my $record = eval {
292                   $tmpresults[ $i - 1 ]->record( $j - 1 )->raw()
293                   ;    # 0 indexed
294                 };
295                 push @{$results}, $record if defined $record;
296             }
297         }
298     );
299
300     foreach my $zoom_query (@zoom_queries) {
301         $zoom_query->destroy();
302     }
303
304     return ( undef, $results, $total_hits );
305 }
306
307 =head2 getRecords
308
309 ( undef, $results_hashref, \@facets_loop ) = getRecords (
310
311         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
312         $results_per_page, $offset,       $expanded_facet, $branches,$itemtypes,
313         $query_type,       $scan
314     );
315
316 The all singing, all dancing, multi-server, asynchronous, scanning,
317 searching, record nabbing, facet-building
318
319 See verbse embedded documentation.
320
321 =cut
322
323 sub getRecords {
324     my (
325         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
326         $results_per_page, $offset,       $expanded_facet, $branches,
327         $itemtypes,        $query_type,   $scan,           $opac
328     ) = @_;
329
330     my @servers = @$servers_ref;
331     my @sort_by = @$sort_by_ref;
332
333     # Initialize variables for the ZOOM connection and results object
334     my $zconn;
335     my @zconns;
336     my @results;
337     my $results_hashref = ();
338
339     # Initialize variables for the faceted results objects
340     my $facets_counter = {};
341     my $facets_info    = {};
342     my $facets         = getFacets();
343
344     my @facets_loop;    # stores the ref to array of hashes for template facets loop
345
346     ### LOOP THROUGH THE SERVERS
347     for ( my $i = 0 ; $i < @servers ; $i++ ) {
348         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
349
350 # perform the search, create the results objects
351 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
352         my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
353
354         #$query_to_use = $simple_query if $scan;
355         warn $simple_query if ( $scan and $DEBUG );
356
357         # Check if we've got a query_type defined, if so, use it
358         eval {
359             if ($query_type) {
360                 if ($query_type =~ /^ccl/) {
361                     $query_to_use =~ s/\:/\=/g;    # change : to = last minute (FIXME)
362                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
363                 } elsif ($query_type =~ /^cql/) {
364                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
365                 } elsif ($query_type =~ /^pqf/) {
366                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
367                 } else {
368                     warn "Unknown query_type '$query_type'.  Results undetermined.";
369                 }
370             } elsif ($scan) {
371                     $results[$i] = $zconns[$i]->scan(  new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
372             } else {
373                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
374             }
375         };
376         if ($@) {
377             warn "WARNING: query problem with $query_to_use " . $@;
378         }
379
380         # Concatenate the sort_by limits and pass them to the results object
381         # Note: sort will override rank
382         my $sort_by;
383         foreach my $sort (@sort_by) {
384             if ( $sort eq "author_az" || $sort eq "author_asc" ) {
385                 $sort_by .= "1=1003 <i ";
386             }
387             elsif ( $sort eq "author_za" || $sort eq "author_dsc" ) {
388                 $sort_by .= "1=1003 >i ";
389             }
390             elsif ( $sort eq "popularity_asc" ) {
391                 $sort_by .= "1=9003 <i ";
392             }
393             elsif ( $sort eq "popularity_dsc" ) {
394                 $sort_by .= "1=9003 >i ";
395             }
396             elsif ( $sort eq "call_number_asc" ) {
397                 $sort_by .= "1=8007  <i ";
398             }
399             elsif ( $sort eq "call_number_dsc" ) {
400                 $sort_by .= "1=8007 >i ";
401             }
402             elsif ( $sort eq "pubdate_asc" ) {
403                 $sort_by .= "1=31 <i ";
404             }
405             elsif ( $sort eq "pubdate_dsc" ) {
406                 $sort_by .= "1=31 >i ";
407             }
408             elsif ( $sort eq "acqdate_asc" ) {
409                 $sort_by .= "1=32 <i ";
410             }
411             elsif ( $sort eq "acqdate_dsc" ) {
412                 $sort_by .= "1=32 >i ";
413             }
414             elsif ( $sort eq "title_az" || $sort eq "title_asc" ) {
415                 $sort_by .= "1=4 <i ";
416             }
417             elsif ( $sort eq "title_za" || $sort eq "title_dsc" ) {
418                 $sort_by .= "1=4 >i ";
419             }
420             else {
421                 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
422             }
423         }
424         if ( $sort_by && !$scan && $results[$i] ) {
425             if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
426                 warn "WARNING sort $sort_by failed";
427             }
428         }
429     }    # finished looping through servers
430
431     # The big moment: asynchronously retrieve results from all servers
432         _ZOOM_event_loop(
433             \@zconns,
434             \@results,
435             sub {
436                 my ( $i, $size ) = @_;
437                 my $results_hash;
438
439                 # loop through the results
440                 $results_hash->{'hits'} = $size;
441                 my $times;
442                 if ( $offset + $results_per_page <= $size ) {
443                     $times = $offset + $results_per_page;
444                 }
445                 else {
446                     $times = $size;
447                 }
448
449                 for ( my $j = $offset ; $j < $times ; $j++ ) {
450                     my $records_hash;
451                     my $record;
452
453                     ## Check if it's an index scan
454                     if ($scan) {
455                         my ( $term, $occ ) = $results[ $i - 1 ]->display_term($j);
456
457                  # here we create a minimal MARC record and hand it off to the
458                  # template just like a normal result ... perhaps not ideal, but
459                  # it works for now
460                         my $tmprecord = MARC::Record->new();
461                         $tmprecord->encoding('UTF-8');
462                         my $tmptitle;
463                         my $tmpauthor;
464
465                 # the minimal record in author/title (depending on MARC flavour)
466                         if ( C4::Context->preference("marcflavour") eq
467                             "UNIMARC" )
468                         {
469                             $tmptitle = MARC::Field->new(
470                                 '200', ' ', ' ',
471                                 a => $term,
472                                 f => $occ
473                             );
474                             $tmprecord->append_fields($tmptitle);
475                         }
476                         else {
477                             $tmptitle =
478                               MARC::Field->new( '245', ' ', ' ', a => $term, );
479                             $tmpauthor =
480                               MARC::Field->new( '100', ' ', ' ', a => $occ, );
481                             $tmprecord->append_fields($tmptitle);
482                             $tmprecord->append_fields($tmpauthor);
483                         }
484                         $results_hash->{'RECORDS'}[$j] =
485                           $tmprecord->as_usmarc();
486                     }
487
488                     # not an index scan
489                     else {
490                         $record = $results[ $i - 1 ]->record($j)->raw();
491                         # warn "RECORD $j:".$record;
492                         $results_hash->{'RECORDS'}[$j] = $record;
493                     }
494
495                 }
496                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
497
498                 # Fill the facets while we're looping, but only for the
499                 # biblioserver and not for a scan
500                 if ( !$scan && $servers[ $i - 1 ] =~ /biblioserver/ ) {
501                     $facets_counter = GetFacets( $results[ $i - 1 ] );
502                     $facets_info    = _get_facets_info( $facets );
503                 }
504
505                 # BUILD FACETS
506                 if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
507                     for my $link_value (
508                         sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
509                         keys %$facets_counter
510                       )
511                     {
512                         my $expandable;
513                         my $number_of_facets;
514                         my @this_facets_array;
515                         for my $one_facet (
516                             sort {
517                                 $facets_counter->{$link_value}
518                                   ->{$b} <=> $facets_counter->{$link_value}
519                                   ->{$a}
520                             } keys %{ $facets_counter->{$link_value} }
521                           )
522                         {
523                             $number_of_facets++;
524                             if (   ( $number_of_facets <= 5 )
525                                 || ( $expanded_facet eq $link_value )
526                                 || ( $facets_info->{$link_value}->{'expanded'} )
527                               )
528                             {
529
530 # Sanitize the link value : parenthesis, question and exclamation mark will cause errors with CCL
531                                 my $facet_link_value = $one_facet;
532                                 $facet_link_value =~ s/[()!?¡¿؟]/ /g;
533
534                                 # fix the length that will display in the label,
535                                 my $facet_label_value = $one_facet;
536                                 my $facet_max_length  = C4::Context->preference(
537                                     'FacetLabelTruncationLength')
538                                   || 20;
539                                 $facet_label_value =
540                                   substr( $one_facet, 0, $facet_max_length )
541                                   . "..."
542                                   if length($facet_label_value) >
543                                       $facet_max_length;
544
545                             # if it's a branch, label by the name, not the code,
546                                 if ( $link_value =~ /branch/ ) {
547                                     if (   defined $branches
548                                         && ref($branches) eq "HASH"
549                                         && defined $branches->{$one_facet}
550                                         && ref( $branches->{$one_facet} ) eq
551                                         "HASH" )
552                                     {
553                                         $facet_label_value =
554                                           $branches->{$one_facet}
555                                           ->{'branchname'};
556                                     }
557                                     else {
558                                         $facet_label_value = "*";
559                                     }
560                                 }
561
562                           # if it's a itemtype, label by the name, not the code,
563                                 if ( $link_value =~ /itype/ ) {
564                                     if (   defined $itemtypes
565                                         && ref($itemtypes) eq "HASH"
566                                         && defined $itemtypes->{$one_facet}
567                                         && ref( $itemtypes->{$one_facet} ) eq
568                                         "HASH" )
569                                     {
570                                         $facet_label_value =
571                                           $itemtypes->{$one_facet}
572                                           ->{translated_description};
573                                     }
574                                 }
575
576                # also, if it's a location code, use the name instead of the code
577                                 if ( $link_value =~ /location/ ) {
578                                     $facet_label_value =
579                                       GetKohaAuthorisedValueLib( 'LOC',
580                                         $one_facet, $opac );
581                                 }
582
583                 # but we're down with the whole label being in the link's title.
584                                 push @this_facets_array,
585                                   {
586                                     facet_count =>
587                                       $facets_counter->{$link_value}
588                                       ->{$one_facet},
589                                     facet_label_value => $facet_label_value,
590                                     facet_title_value => $one_facet,
591                                     facet_link_value  => $facet_link_value,
592                                     type_link_value   => $link_value,
593                                   }
594                                   if ($facet_label_value);
595                             }
596                         }
597
598                         # handle expanded option
599                         unless ( $facets_info->{$link_value}->{'expanded'} ) {
600                             $expandable = 1
601                               if ( ( $number_of_facets > 5 )
602                                 && ( $expanded_facet ne $link_value ) );
603                         }
604                         push @facets_loop,
605                           {
606                             type_link_value => $link_value,
607                             type_id         => $link_value . "_id",
608                             "type_label_"
609                               . $facets_info->{$link_value}->{'label_value'} =>
610                               1,
611                             facets     => \@this_facets_array,
612                             expandable => $expandable,
613                             expand     => $link_value,
614                           }
615                           unless (
616                             (
617                                 $facets_info->{$link_value}->{'label_value'} =~
618                                 /Libraries/
619                             )
620                             and ( Koha::Libraries->search->count == 1 )
621                           );
622                     }
623                 }
624             }
625         );
626     return ( undef, $results_hashref, \@facets_loop );
627 }
628
629 sub GetFacets {
630
631     my $rs = shift;
632     my $facets;
633
634     my $indexing_mode    = C4::Context->config('zebra_bib_index_mode') // 'dom';
635     my $use_zebra_facets = C4::Context->config('use_zebra_facets') // 0;
636
637     if ( $indexing_mode eq 'dom' &&
638          $use_zebra_facets ) {
639         $facets = _get_facets_from_zebra( $rs );
640     } else {
641         $facets = _get_facets_from_records( $rs );
642     }
643
644     return $facets;
645 }
646
647 sub _get_facets_from_records {
648
649     my $rs = shift;
650
651     my $facets_maxrecs = C4::Context->preference('maxRecordsForFacets') // 20;
652     my $facets_config  = getFacets();
653     my $facets         = {};
654     my $size           = $rs->size();
655     my $jmax           = $size > $facets_maxrecs
656                             ? $facets_maxrecs
657                             : $size;
658
659     for ( my $j = 0 ; $j < $jmax ; $j++ ) {
660
661         my $marc_record = new_record_from_zebra (
662                 'biblioserver',
663                 $rs->record( $j )->raw()
664         );
665
666         if ( ! defined $marc_record ) {
667             warn "ERROR DECODING RECORD - $@: " .
668                 $rs->record( $j )->raw();
669             next;
670         }
671
672         _get_facets_data_from_record( $marc_record, $facets_config, $facets );
673     }
674
675     return $facets;
676 }
677
678 =head2 _get_facets_data_from_record
679
680     C4::Search::_get_facets_data_from_record( $marc_record, $facets, $facets_counter );
681
682 Internal function that extracts facets information from a MARC::Record object
683 and populates $facets_counter for using in getRecords.
684
685 $facets is expected to be filled with C4::Koha::getFacets output (i.e. the configured
686 facets for Zebra).
687
688 =cut
689
690 sub _get_facets_data_from_record {
691
692     my ( $marc_record, $facets, $facets_counter ) = @_;
693
694     for my $facet (@$facets) {
695
696         my @used_datas = ();
697
698         foreach my $tag ( @{ $facet->{ tags } } ) {
699
700             # tag number is the first three digits
701             my $tag_num          = substr( $tag, 0, 3 );
702             # subfields are the remainder
703             my $subfield_letters = substr( $tag, 3 );
704
705             my @fields = $marc_record->field( $tag_num );
706             foreach my $field (@fields) {
707                 # If $field->indicator(1) eq 'z', it means it is a 'see from'
708                 # field introduced because of IncludeSeeFromInSearches, so skip it
709                 next if $field->indicator(1) eq 'z';
710
711                 my $data = $field->as_string( $subfield_letters, $facet->{ sep } );
712
713                 unless ( grep { /^\Q$data\E$/ } @used_datas ) {
714                     push @used_datas, $data;
715                     $facets_counter->{ $facet->{ idx } }->{ $data }++;
716                 }
717             }
718         }
719     }
720 }
721
722 =head2 _get_facets_from_zebra
723
724     my $facets = _get_facets_from_zebra( $result_set )
725
726 Retrieves facets for a specified result set. It loops through the facets defined
727 in C4::Koha::getFacets and returns a hash with the following structure:
728
729    {  facet_idx => {
730             facet_value => count
731       },
732       ...
733    }
734
735 =cut
736
737 sub _get_facets_from_zebra {
738
739     my $rs = shift;
740
741     # save current elementSetName
742     my $elementSetName = $rs->option( 'elementSetName' );
743
744     my $facets_loop = getFacets();
745     my $facets_data  = {};
746     # loop through defined facets and fill the facets hashref
747     foreach my $facet ( @$facets_loop ) {
748
749         my $idx = $facet->{ idx };
750         my $sep = $facet->{ sep };
751         my $facet_values = _get_facet_from_result_set( $idx, $rs, $sep );
752         if ( $facet_values ) {
753             # we've actually got a result
754             $facets_data->{ $idx } = $facet_values;
755         }
756     }
757     # set elementSetName to its previous value to avoid side effects
758     $rs->option( elementSetName => $elementSetName );
759
760     return $facets_data;
761 }
762
763 =head2 _get_facet_from_result_set
764
765     my $facet_values =
766         C4::Search::_get_facet_from_result_set( $facet_idx, $result_set, $sep )
767
768 Internal function that extracts facet information for a specific index ($facet_idx) and
769 returns a hash containing facet values and count:
770
771     {
772         $facet_value => $count ,
773         ...
774     }
775
776 Warning: this function has the side effect of changing the elementSetName for the result
777 set. It is a helper function for the main loop, which takes care of backing it up for
778 restoring.
779
780 =cut
781
782 sub _get_facet_from_result_set {
783
784     my $facet_idx = shift;
785     my $rs        = shift;
786     my $sep       = shift;
787
788     my $internal_sep  = '<*>';
789     my $facetMaxCount = C4::Context->preference('FacetMaxCount') // 20;
790
791     return if ( ! defined $facet_idx || ! defined $rs );
792     # zebra's facet element, untokenized index
793     my $facet_element = 'zebra::facet::' . $facet_idx . ':0:' . $facetMaxCount;
794     # configure zebra results for retrieving the desired facet
795     $rs->option( elementSetName => $facet_element );
796     # get the facet record from result set
797     my $facet = $rs->record( 0 )->raw;
798     # if the facet has no restuls...
799     return if !defined $facet;
800     # TODO: benchmark DOM vs. SAX performance
801     my $facet_dom = XML::LibXML->load_xml(
802       string => ($facet)
803     );
804     my @terms = $facet_dom->getElementsByTagName('term');
805     return if ! @terms;
806
807     my $facets = {};
808     foreach my $term ( @terms ) {
809         my $facet_value = $term->textContent;
810         $facet_value =~ s/\Q$internal_sep\E/$sep/ if defined $sep;
811         $facets->{ $facet_value } = $term->getAttribute( 'occur' );
812     }
813
814     return $facets;
815 }
816
817 =head2 _get_facets_info
818
819     my $facets_info = C4::Search::_get_facets_info( $facets )
820
821 Internal function that extracts facets information and properly builds
822 the data structure needed to render facet labels.
823
824 =cut
825
826 sub _get_facets_info {
827
828     my $facets = shift;
829
830     my $facets_info = {};
831
832     for my $facet ( @$facets ) {
833         $facets_info->{ $facet->{ idx } }->{ label_value } = $facet->{ label };
834         $facets_info->{ $facet->{ idx } }->{ expanded }    = $facet->{ expanded };
835     }
836
837     return $facets_info;
838 }
839
840 sub pazGetRecords {
841     my (
842         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
843         $results_per_page, $offset,       $expanded_facet, $branches,
844         $query_type,       $scan
845     ) = @_;
846
847     my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
848     $paz->init();
849     $paz->search($simple_query);
850     sleep 1;   # FIXME: WHY?
851
852     # do results
853     my $results_hashref = {};
854     my $stats = XMLin($paz->stat);
855     my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
856
857     # for a grouped search result, the number of hits
858     # is the number of groups returned; 'bib_hits' will have
859     # the total number of bibs.
860     $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
861     $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
862
863     HIT: foreach my $hit (@{ $results->{'hit'} }) {
864         my $recid = $hit->{recid}->[0];
865
866         my $work_title = $hit->{'md-work-title'}->[0];
867         my $work_author;
868         if (exists $hit->{'md-work-author'}) {
869             $work_author = $hit->{'md-work-author'}->[0];
870         }
871         my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
872
873         my $result_group = {};
874         $result_group->{'group_label'} = $group_label;
875         $result_group->{'group_merge_key'} = $recid;
876
877         my $count = 1;
878         if (exists $hit->{count}) {
879             $count = $hit->{count}->[0];
880         }
881         $result_group->{'group_count'} = $count;
882
883         for (my $i = 0; $i < $count; $i++) {
884             # FIXME -- may need to worry about diacritics here
885             my $rec = $paz->record($recid, $i);
886             push @{ $result_group->{'RECORDS'} }, $rec;
887         }
888
889         push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
890     }
891
892     # pass through facets
893     my $termlist_xml = $paz->termlist('author,subject');
894     my $terms = XMLin($termlist_xml, forcearray => 1);
895     my @facets_loop = ();
896     #die Dumper($results);
897 #    foreach my $list (sort keys %{ $terms->{'list'} }) {
898 #        my @facets = ();
899 #        foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
900 #            push @facets, {
901 #                facet_label_value => $facet->{'name'}->[0],
902 #            };
903 #        }
904 #        push @facets_loop, ( {
905 #            type_label => $list,
906 #            facets => \@facets,
907 #        } );
908 #    }
909
910     return ( undef, $results_hashref, \@facets_loop );
911 }
912
913 # TRUNCATION
914 sub _detect_truncation {
915     my ( $operand, $index ) = @_;
916     my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
917         @regexpr );
918     $operand =~ s/^ //g;
919     my @wordlist = split( /\s/, $operand );
920     foreach my $word (@wordlist) {
921         if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
922             push @rightlefttruncated, $word;
923         }
924         elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
925             push @lefttruncated, $word;
926         }
927         elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
928             push @righttruncated, $word;
929         }
930         elsif ( index( $word, "*" ) < 0 ) {
931             push @nontruncated, $word;
932         }
933         else {
934             push @regexpr, $word;
935         }
936     }
937     return (
938         \@nontruncated,       \@righttruncated, \@lefttruncated,
939         \@rightlefttruncated, \@regexpr
940     );
941 }
942
943 # STEMMING
944 sub _build_stemmed_operand {
945     my ($operand,$lang) = @_;
946     require Lingua::Stem::Snowball ;
947     my $stemmed_operand=q{};
948
949     # If operand contains a digit, it is almost certainly an identifier, and should
950     # not be stemmed.  This is particularly relevant for ISBNs and ISSNs, which
951     # can contain the letter "X" - for example, _build_stemmend_operand would reduce
952     # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
953     # results (e.g., "23 x 29 cm." from the 300$c).  Bug 2098.
954     return $operand if $operand =~ /\d/;
955
956 # FIXME: the locale should be set based on the user's language and/or search choice
957     #warn "$lang";
958     # Make sure we only use the first two letters from the language code
959     $lang = lc(substr($lang, 0, 2));
960     # The language codes for the two variants of Norwegian will now be "nb" and "nn",
961     # none of which Lingua::Stem::Snowball can use, so we need to "translate" them
962     if ($lang eq 'nb' || $lang eq 'nn') {
963       $lang = 'no';
964     }
965     my $stemmer = Lingua::Stem::Snowball->new( lang => $lang,
966                                                encoding => "UTF-8" );
967
968     my @words = split( / /, $operand );
969     my @stems = $stemmer->stem(\@words);
970     for my $stem (@stems) {
971         $stemmed_operand .= "$stem";
972         $stemmed_operand .= "?"
973           unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
974         $stemmed_operand .= " ";
975     }
976     warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
977     return $stemmed_operand;
978 }
979
980 # FIELD WEIGHTING
981 sub _build_weighted_query {
982
983 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
984 # pretty well but could work much better if we had a smarter query parser
985     my ( $operand, $stemmed_operand, $index ) = @_;
986     my $stemming      = C4::Context->preference("QueryStemming")     || 0;
987     my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
988     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy")        || 0;
989     $operand =~ s/"/ /g;    # Bug 7518: searches with quotation marks don't work
990
991     my $weighted_query .= "(rk=(";    # Specifies that we're applying rank
992
993     # Keyword, or, no index specified
994     if ( ( $index eq 'kw' ) || ( !$index ) ) {
995         $weighted_query .=
996           "Title-cover,ext,r1=\"$operand\"";    # exact title-cover
997         $weighted_query .= " or ti,ext,r2=\"$operand\"";    # exact title
998         $weighted_query .= " or Title-cover,phr,r3=\"$operand\"";    # phrase title
999         $weighted_query .= " or ti,wrdl,r4=\"$operand\"";    # words in title
1000           #$weighted_query .= " or any,ext,r4=$operand";               # exact any
1001           #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # word list any
1002         $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
1003           if $fuzzy_enabled;    # add fuzzy, word list
1004         $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
1005           if ( $stemming and $stemmed_operand )
1006           ;                     # add stemming, right truncation
1007         $weighted_query .= " or wrdl,r9=\"$operand\"";
1008
1009         # embedded sorting: 0 a-z; 1 z-a
1010         # $weighted_query .= ") or (sort1,aut=1";
1011     }
1012
1013     # Barcode searches should skip this process
1014     elsif ( $index eq 'bc' ) {
1015         $weighted_query .= "bc=\"$operand\"";
1016     }
1017
1018     # Authority-number searches should skip this process
1019     elsif ( $index eq 'an' ) {
1020         $weighted_query .= "an=\"$operand\"";
1021     }
1022
1023     # If the index already has more than one qualifier, wrap the operand
1024     # in quotes and pass it back (assumption is that the user knows what they
1025     # are doing and won't appreciate us mucking up their query
1026     elsif ( $index =~ ',' ) {
1027         $weighted_query .= " $index=\"$operand\"";
1028     }
1029
1030     #TODO: build better cases based on specific search indexes
1031     else {
1032         $weighted_query .= " $index,ext,r1=\"$operand\"";    # exact index
1033           #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
1034         $weighted_query .= " or $index,phr,r3=\"$operand\"";    # phrase index
1035         $weighted_query .= " or $index,wrdl,r6=\"$operand\"";    # word list index
1036         $weighted_query .= " or $index,wrdl,fuzzy,r8=\"$operand\""
1037           if $fuzzy_enabled;    # add fuzzy, word list
1038         $weighted_query .= " or $index,wrdl,rt,r9=\"$stemmed_operand\""
1039           if ( $stemming and $stemmed_operand );    # add stemming, right truncation
1040     }
1041
1042     $weighted_query .= "))";                       # close rank specification
1043     return $weighted_query;
1044 }
1045
1046 =head2 getIndexes
1047
1048 Return an array with available indexes.
1049
1050 =cut
1051
1052 sub getIndexes{
1053     my @indexes = (
1054                     # biblio indexes
1055                     'ab',
1056                     'Abstract',
1057                     'acqdate',
1058                     'allrecords',
1059                     'an',
1060                     'Any',
1061                     'at',
1062                     'au',
1063                     'aub',
1064                     'aud',
1065                     'audience',
1066                     'auo',
1067                     'aut',
1068                     'Author',
1069                     'Author-in-order ',
1070                     'Author-personal-bibliography',
1071                     'Authority-Number',
1072                     'authtype',
1073                     'bc',
1074                     'Bib-level',
1075                     'biblionumber',
1076                     'bio',
1077                     'biography',
1078                     'callnum',
1079                     'cfn',
1080                     'Chronological-subdivision',
1081                     'cn-bib-source',
1082                     'cn-bib-sort',
1083                     'cn-class',
1084                     'cn-item',
1085                     'cn-prefix',
1086                     'cn-suffix',
1087                     'cpn',
1088                     'Code-institution',
1089                     'Conference-name',
1090                     'Conference-name-heading',
1091                     'Conference-name-see',
1092                     'Conference-name-seealso',
1093                     'Content-type',
1094                     'Control-number',
1095                     'copydate',
1096                     'Corporate-name',
1097                     'Corporate-name-heading',
1098                     'Corporate-name-see',
1099                     'Corporate-name-seealso',
1100                     'Country-publication',
1101                     'ctype',
1102                     'curriculum',
1103                     'date-entered-on-file',
1104                     'Date-of-acquisition',
1105                     'Date-of-publication',
1106                     'Date-time-last-modified',
1107                     'Dewey-classification',
1108                     'Dissertation-information',
1109                     'diss',
1110                     'dtlm',
1111                     'EAN',
1112                     'extent',
1113                     'fic',
1114                     'fiction',
1115                     'Form-subdivision',
1116                     'format',
1117                     'Geographic-subdivision',
1118                     'he',
1119                     'Heading',
1120                     'Heading-use-main-or-added-entry',
1121                     'Heading-use-series-added-entry ',
1122                     'Heading-use-subject-added-entry',
1123                     'Host-item',
1124                     'id-other',
1125                     'Illustration-code',
1126                     'Index-term-genre',
1127                     'Index-term-uncontrolled',
1128                     'ISBN',
1129                     'isbn',
1130                     'ISSN',
1131                     'issn',
1132                     'itemtype',
1133                     'kw',
1134                     'Koha-Auth-Number',
1135                     'l-format',
1136                     'language',
1137                     'language-original',
1138                     'lc-card',
1139                     'LC-card-number',
1140                     'lcn',
1141                     'lex',
1142                     'llength',
1143                     'ln',
1144                     'ln-audio',
1145                     'ln-subtitle',
1146                     'Local-classification',
1147                     'Local-number',
1148                     'Match-heading',
1149                     'Match-heading-see-from',
1150                     'Material-type',
1151                     'mc-itemtype',
1152                     'mc-rtype',
1153                     'mus',
1154                     'name',
1155                     'Music-number',
1156                     'Name-geographic',
1157                     'Name-geographic-heading',
1158                     'Name-geographic-see',
1159                     'Name-geographic-seealso',
1160                     'nb',
1161                     'Note',
1162                     'notes',
1163                     'ns',
1164                     'nt',
1165                     'pb',
1166                     'Personal-name',
1167                     'Personal-name-heading',
1168                     'Personal-name-see',
1169                     'Personal-name-seealso',
1170                     'pl',
1171                     'Place-publication',
1172                     'pn',
1173                     'popularity',
1174                     'pubdate',
1175                     'Publisher',
1176                     'Provider',
1177                     'pv',
1178                     'Record-control-number',
1179                     'rcn',
1180                     'Record-type',
1181                     'rtype',
1182                     'se',
1183                     'See',
1184                     'See-also',
1185                     'sn',
1186                     'Stock-number',
1187                     'su',
1188                     'Subject',
1189                     'Subject-heading-thesaurus',
1190                     'Subject-name-personal',
1191                     'Subject-subdivision',
1192                     'Summary',
1193                     'Suppress',
1194                     'su-geo',
1195                     'su-na',
1196                     'su-to',
1197                     'su-ut',
1198                     'ut',
1199                     'Term-genre-form',
1200                     'Term-genre-form-heading',
1201                     'Term-genre-form-see',
1202                     'Term-genre-form-seealso',
1203                     'ti',
1204                     'Title',
1205                     'Title-cover',
1206                     'Title-series',
1207                     'Title-uniform',
1208                     'Title-uniform-heading',
1209                     'Title-uniform-see',
1210                     'Title-uniform-seealso',
1211                     'totalissues',
1212                     'yr',
1213
1214                     # items indexes
1215                     'acqsource',
1216                     'barcode',
1217                     'bc',
1218                     'branch',
1219                     'ccode',
1220                     'classification-source',
1221                     'cn-sort',
1222                     'coded-location-qualifier',
1223                     'copynumber',
1224                     'damaged',
1225                     'datelastborrowed',
1226                     'datelastseen',
1227                     'holdingbranch',
1228                     'homebranch',
1229                     'issues',
1230                     'item',
1231                     'itemnumber',
1232                     'itype',
1233                     'Local-classification',
1234                     'location',
1235                     'lost',
1236                     'materials-specified',
1237                     'mc-ccode',
1238                     'mc-itype',
1239                     'mc-loc',
1240                     'notforloan',
1241                     'Number-local-acquisition',
1242                     'onloan',
1243                     'price',
1244                     'renewals',
1245                     'replacementprice',
1246                     'replacementpricedate',
1247                     'reserves',
1248                     'restricted',
1249                     'stack',
1250                     'stocknumber',
1251                     'inv',
1252                     'uri',
1253                     'withdrawn',
1254
1255                     # subject related
1256                   );
1257
1258     return \@indexes;
1259 }
1260
1261 =head2 _handle_exploding_index
1262
1263     my $query = _handle_exploding_index($index, $term)
1264
1265 Callback routine to generate the search for "exploding" indexes (i.e.
1266 those indexes which are turned into multiple or-connected searches based
1267 on authority data).
1268
1269 =cut
1270
1271 sub _handle_exploding_index {
1272     my ($QParser, $filter, $params, $negate, $server) = @_;
1273     my $index = $filter;
1274     my $term = join(' ', @$params);
1275
1276     return unless ($index =~ m/(su-br|su-na|su-rl)/ && $term);
1277
1278     my $marcflavour = C4::Context->preference('marcflavour');
1279
1280     my $codesubfield = $marcflavour eq 'UNIMARC' ? '5' : 'w';
1281     my $wantedcodes = '';
1282     my @subqueries = ( "\@attr 1=Subject \@attr 4=1 \"$term\"");
1283     my ($error, $results, $total_hits) = SimpleSearch( "he:$term", undef, undef, [ "authorityserver" ] );
1284     foreach my $auth (@$results) {
1285         my $record = MARC::Record->new_from_usmarc($auth);
1286         my @references = $record->field('5..');
1287         if (@references) {
1288             if ($index eq 'su-br') {
1289                 $wantedcodes = 'g';
1290             } elsif ($index eq 'su-na') {
1291                 $wantedcodes = 'h';
1292             } elsif ($index eq 'su-rl') {
1293                 $wantedcodes = '';
1294             }
1295             foreach my $reference (@references) {
1296                 my $codes = $reference->subfield($codesubfield);
1297                 push @subqueries, '@attr 1=Subject @attr 4=1 "' . $reference->as_string('abcdefghijlmnopqrstuvxyz') . '"' if (($codes && $codes eq $wantedcodes) || !$wantedcodes);
1298             }
1299         }
1300     }
1301     my $query = ' @or ' x (scalar(@subqueries) - 1) . join(' ', @subqueries);
1302     return $query;
1303 }
1304
1305 =head2 parseQuery
1306
1307     ( $operators, $operands, $indexes, $limits,
1308       $sort_by, $scan, $lang ) =
1309             buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1310
1311 Shim function to ease the transition from buildQuery to a new QueryParser.
1312 This function is called at the beginning of buildQuery, and modifies
1313 buildQuery's input. If it can handle the input, it returns a query that
1314 buildQuery will not try to parse.
1315 =cut
1316
1317 sub parseQuery {
1318     my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
1319
1320     my @operators = $operators ? @$operators : ();
1321     my @indexes   = $indexes   ? @$indexes   : ();
1322     my @operands  = $operands  ? @$operands  : ();
1323     my @limits    = $limits    ? @$limits    : ();
1324     my @sort_by   = $sort_by   ? @$sort_by   : ();
1325
1326     my $query = $operands[0];
1327     my $index;
1328     my $term;
1329     my $query_desc;
1330
1331     my $QParser;
1332     $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') || $query =~ s/^qp=//);
1333     undef $QParser if ($query =~ m/^(ccl=|pqf=|cql=)/ || grep (/\w,\w|\w=\w/, @operands, @indexes) );
1334     undef $QParser if (scalar @limits > 0);
1335
1336     if ($QParser)
1337     {
1338         $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
1339         $query = '';
1340         for ( my $ii = 0 ; $ii <= @operands ; $ii++ ) {
1341             next unless $operands[$ii];
1342             $query .= $operators[ $ii - 1 ] eq 'or' ? ' || ' : ' && '
1343               if ($query);
1344             if ( $operands[$ii] =~ /^[^"]\W*[-|_\w]*:\w.*[^"]$/ ) {
1345                 $query .= $operands[$ii];
1346             }
1347             elsif ( $indexes[$ii] =~ m/su-/ ) {
1348                 $query .= $indexes[$ii] . '(' . $operands[$ii] . ')';
1349             }
1350             else {
1351                 $query .=
1352                   ( $indexes[$ii] ? "$indexes[$ii]:" : '' ) . $operands[$ii];
1353             }
1354         }
1355         foreach my $limit (@limits) {
1356         }
1357         if ( scalar(@sort_by) > 0 ) {
1358             my $modifier_re =
1359               '#(' . join( '|', @{ $QParser->modifiers } ) . ')';
1360             $query =~ s/$modifier_re//g;
1361             foreach my $modifier (@sort_by) {
1362                 $query .= " #$modifier";
1363             }
1364         }
1365
1366         $query_desc = $query;
1367         $query_desc =~ s/\s+/ /g;
1368         if ( C4::Context->preference("QueryWeightFields") ) {
1369         }
1370         $QParser->add_bib1_filter_map( 'su-br' => 'biblioserver' =>
1371               { 'target_syntax_callback' => \&_handle_exploding_index } );
1372         $QParser->add_bib1_filter_map( 'su-na' => 'biblioserver' =>
1373               { 'target_syntax_callback' => \&_handle_exploding_index } );
1374         $QParser->add_bib1_filter_map( 'su-rl' => 'biblioserver' =>
1375               { 'target_syntax_callback' => \&_handle_exploding_index } );
1376         $QParser->parse($query);
1377         $operands[0] = "pqf=" . $QParser->target_syntax('biblioserver');
1378     }
1379     else {
1380         require Koha::QueryParser::Driver::PQF;
1381         my $modifier_re = '#(' . join( '|', @{Koha::QueryParser::Driver::PQF->modifiers}) . ')';
1382         s/$modifier_re//g for @operands;
1383     }
1384
1385     return ( $operators, \@operands, $indexes, $limits, $sort_by, $scan, $lang, $query_desc);
1386 }
1387
1388 =head2 buildQuery
1389
1390 ( $error, $query,
1391 $simple_query, $query_cgi,
1392 $query_desc, $limit,
1393 $limit_cgi, $limit_desc,
1394 $query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1395
1396 Build queries and limits in CCL, CGI, Human,
1397 handle truncation, stemming, field weighting, fuzziness, etc.
1398
1399 See verbose embedded documentation.
1400
1401
1402 =cut
1403
1404 sub buildQuery {
1405     my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
1406
1407     warn "---------\nEnter buildQuery\n---------" if $DEBUG;
1408
1409     my $query_desc;
1410     ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang, $query_desc) = parseQuery($operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1411
1412     # dereference
1413     my @operators = $operators ? @$operators : ();
1414     my @indexes   = $indexes   ? @$indexes   : ();
1415     my @operands  = $operands  ? @$operands  : ();
1416     my @limits    = $limits    ? @$limits    : ();
1417     my @sort_by   = $sort_by   ? @$sort_by   : ();
1418
1419     my $stemming         = C4::Context->preference("QueryStemming")        || 0;
1420     my $auto_truncation  = C4::Context->preference("QueryAutoTruncate")    || 0;
1421     my $weight_fields    = C4::Context->preference("QueryWeightFields")    || 0;
1422     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
1423
1424     my $query        = $operands[0];
1425     my $simple_query = $operands[0];
1426
1427     # initialize the variables we're passing back
1428     my $query_cgi;
1429     my $query_type;
1430
1431     my $limit;
1432     my $limit_cgi;
1433     my $limit_desc;
1434
1435     my $cclq       = 0;
1436     my $cclindexes = getIndexes();
1437     if ( $query !~ /\s*(ccl=|pqf=|cql=)/ ) {
1438         while ( !$cclq && $query =~ /(?:^|\W)([\w-]+)(,[\w-]+)*[:=]/g ) {
1439             my $dx = lc($1);
1440             $cclq = grep { lc($_) eq $dx } @$cclindexes;
1441         }
1442         $query = "ccl=$query" if $cclq;
1443     }
1444
1445 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
1446 # DIAGNOSTIC ONLY!!
1447     if ( $query =~ /^ccl=/ ) {
1448         my $q=$';
1449         # This is needed otherwise ccl= and &limit won't work together, and
1450         # this happens when selecting a subject on the opac-detail page
1451         @limits = grep {!/^$/} @limits;
1452         if ( @limits ) {
1453             $q .= ' and '.join(' and ', @limits);
1454         }
1455         return ( undef, $q, $q, "q=ccl=".uri_escape_utf8($q), $q, '', '', '', 'ccl' );
1456     }
1457     if ( $query =~ /^cql=/ ) {
1458         return ( undef, $', $', "q=cql=".uri_escape_utf8($'), $', '', '', '', 'cql' );
1459     }
1460     if ( $query =~ /^pqf=/ ) {
1461         if ($query_desc) {
1462             $query_cgi = "q=".uri_escape_utf8($query_desc);
1463         } else {
1464             $query_desc = $';
1465             $query_cgi = "q=pqf=".uri_escape_utf8($');
1466         }
1467         return ( undef, $', $', $query_cgi, $query_desc, '', '', '', 'pqf' );
1468     }
1469
1470     # pass nested queries directly
1471     # FIXME: need better handling of some of these variables in this case
1472     # Nested queries aren't handled well and this implementation is flawed and causes users to be
1473     # unable to search for anything containing () commenting out, will be rewritten for 3.4.0
1474 #    if ( $query =~ /(\(|\))/ ) {
1475 #        return (
1476 #            undef,              $query, $simple_query, $query_cgi,
1477 #            $query,             $limit, $limit_cgi,    $limit_desc,
1478 #            'ccl'
1479 #        );
1480 #    }
1481
1482 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
1483 # query operands and indexes and add stemming, truncation, field weighting, etc.
1484 # Once we do so, we'll end up with a value in $query, just like if we had an
1485 # incoming $query from the user
1486     else {
1487         $query = ""
1488           ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
1489         my $previous_operand
1490           ;    # a flag used to keep track if there was a previous query
1491                # if there was, we can apply the current operator
1492                # for every operand
1493         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
1494
1495             # COMBINE OPERANDS, INDEXES AND OPERATORS
1496             if ( $operands[$i] ) {
1497                 $operands[$i]=~s/^\s+//;
1498
1499               # A flag to determine whether or not to add the index to the query
1500                 my $indexes_set;
1501
1502 # If the user is sophisticated enough to specify an index, turn off field weighting, and stemming handling
1503                 if ( $operands[$i] =~ /\w(:|=)/ || $scan ) {
1504                     $weight_fields    = 0;
1505                     $stemming         = 0;
1506                 } else {
1507                     $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
1508                 }
1509                 my $operand = $operands[$i];
1510                 my $index   = $indexes[$i];
1511
1512                 # Add index-specific attributes
1513
1514                 #Afaik, this 'yr' condition will only ever be met in the staff client advanced search
1515                 #for "Publication date", since typing 'yr:YYYY' into the search box produces a CCL query,
1516                 #which is processed higher up in this sub. Other than that, year searches are typically
1517                 #handled as limits which are not processed her either.
1518
1519                 # Date of Publication
1520                 if ( $index =~ /yr/ ) {
1521                     #weight_fields/relevance search causes errors with date ranges
1522                     #In the case of YYYY-, it will only return records with a 'yr' of YYYY (not the range)
1523                     #In the case of YYYY-YYYY, it will return no results
1524                     $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = 0;
1525                 }
1526
1527                 # Date of Acquisition
1528                 elsif ( $index =~ /acqdate/ ) {
1529                     #stemming and auto_truncation would have zero impact since it already is YYYY-MM-DD format
1530                     #Weight_fields probably SHOULD be turned OFF, otherwise you'll get records floating to the
1531                       #top of the results just because they have lots of item records matching that date.
1532                     #Fuzzy actually only applies during _build_weighted_query, and is reset there anyway, so
1533                       #irrelevant here
1534                     $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = 0;
1535                 }
1536                 # ISBN,ISSN,Standard Number, don't need special treatment
1537                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
1538                     (
1539                         $stemming,      $auto_truncation,
1540                         $weight_fields, $fuzzy_enabled
1541                     ) = ( 0, 0, 0, 0 );
1542
1543                     if ( $index eq 'nb' ) {
1544                         if ( C4::Context->preference("SearchWithISBNVariations") ) {
1545                             my @isbns = C4::Koha::GetVariationsOfISBN( $operand );
1546                             $operands[$i] = $operand =  '(nb=' . join(' OR nb=', @isbns) . ')';
1547                             $indexes[$i] = $index = '';
1548                         }
1549                     }
1550                 }
1551
1552                 if(not $index){
1553                     $index = 'kw';
1554                 }
1555
1556                 # Set default structure attribute (word list)
1557                 my $struct_attr = q{};
1558                 unless ( $indexes_set || !$index || $index =~ /,(st-|phr|ext|wrdl)/ || $index =~ /^(nb|ns)$/ ) {
1559                     $struct_attr = ",wrdl";
1560                 }
1561
1562                 # Some helpful index variants
1563                 my $index_plus       = $index . $struct_attr . ':';
1564                 my $index_plus_comma = $index . $struct_attr . ',';
1565
1566                 if ($auto_truncation){
1567                         unless ( $index =~ /,(st-|phr|ext)/ ) {
1568                                                 #FIXME only valid with LTR scripts
1569                                                 $operand=join(" ",map{
1570                                                                                         (index($_,"*")>0?"$_":"$_*")
1571                                                                                          }split (/\s+/,$operand));
1572                                                 warn $operand if $DEBUG;
1573                                         }
1574                                 }
1575
1576                 # Detect Truncation
1577                 my $truncated_operand;
1578                 my( $nontruncated, $righttruncated, $lefttruncated,
1579                     $rightlefttruncated, $regexpr
1580                 ) = _detect_truncation( $operand, $index );
1581                 warn
1582 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1583                   if $DEBUG;
1584
1585                 # Apply Truncation
1586                 if (
1587                     scalar(@$righttruncated) + scalar(@$lefttruncated) +
1588                     scalar(@$rightlefttruncated) > 0 )
1589                 {
1590
1591                # Don't field weight or add the index to the query, we do it here
1592                     $indexes_set = 1;
1593                     undef $weight_fields;
1594                     my $previous_truncation_operand;
1595                     if (scalar @$nontruncated) {
1596                         $truncated_operand .= "$index_plus @$nontruncated ";
1597                         $previous_truncation_operand = 1;
1598                     }
1599                     if (scalar @$righttruncated) {
1600                         $truncated_operand .= "and " if $previous_truncation_operand;
1601                         $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1602                         $previous_truncation_operand = 1;
1603                     }
1604                     if (scalar @$lefttruncated) {
1605                         $truncated_operand .= "and " if $previous_truncation_operand;
1606                         $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1607                         $previous_truncation_operand = 1;
1608                     }
1609                     if (scalar @$rightlefttruncated) {
1610                         $truncated_operand .= "and " if $previous_truncation_operand;
1611                         $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1612                         $previous_truncation_operand = 1;
1613                     }
1614                 }
1615                 $operand = $truncated_operand if $truncated_operand;
1616                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1617
1618                 # Handle Stemming
1619                 my $stemmed_operand;
1620                 $stemmed_operand = _build_stemmed_operand($operand, $lang)
1621                                                                                 if $stemming;
1622
1623                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1624
1625                 # Handle Field Weighting
1626                 my $weighted_operand;
1627                 if ($weight_fields) {
1628                     $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1629                     $operand = $weighted_operand;
1630                     $indexes_set = 1;
1631                 }
1632
1633                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1634
1635                 ($query,$query_cgi,$query_desc,$previous_operand) = _build_initial_query({
1636                     query => $query,
1637                     query_cgi => $query_cgi,
1638                     query_desc => $query_desc,
1639                     operator => ($operators[ $i - 1 ]) ? $operators[ $i - 1 ] : '',
1640                     parsed_operand => $operand,
1641                     original_operand => ($operands[$i]) ? $operands[$i] : '',
1642                     index => $index,
1643                     index_plus => $index_plus,
1644                     indexes_set => $indexes_set,
1645                     previous_operand => $previous_operand,
1646                 });
1647
1648             }    #/if $operands
1649         }    # /for
1650     }
1651     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1652
1653     # add limits
1654     my %group_OR_limits;
1655     my $availability_limit;
1656     foreach my $this_limit (@limits) {
1657         next unless $this_limit;
1658         if ( $this_limit =~ /available/ ) {
1659 #
1660 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1661 ## In English:
1662 ## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1663             $availability_limit .=
1664 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1665             $limit_cgi  .= "&limit=available";
1666             $limit_desc .= "";
1667         }
1668
1669         # group_OR_limits, prefixed by mc-
1670         # OR every member of the group
1671         elsif ( $this_limit =~ /mc/ ) {
1672             my ($k,$v) = split(/:/, $this_limit,2);
1673             if ( $k !~ /mc-i(tem)?type/ ) {
1674                 # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
1675                 $this_limit =~ tr/"//d;
1676                 $this_limit = $k.":'".$v."'";
1677             }
1678
1679             $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
1680             $limit_desc      .= " or " if $group_OR_limits{$k};
1681             $group_OR_limits{$k} .= "$this_limit";
1682             $limit_cgi       .= "&limit=" . uri_escape_utf8($this_limit);
1683             $limit_desc      .= " $this_limit";
1684         }
1685
1686         # Regular old limits
1687         else {
1688             $limit .= " and " if $limit || $query;
1689             $limit      .= "$this_limit";
1690             $limit_cgi  .= "&limit=" . uri_escape_utf8($this_limit);
1691             if ($this_limit =~ /^branch:(.+)/) {
1692                 my $branchcode = $1;
1693                 my $branchname = GetBranchName($branchcode);
1694                 if (defined $branchname) {
1695                     $limit_desc .= " branch:$branchname";
1696                 } else {
1697                     $limit_desc .= " $this_limit";
1698                 }
1699             } else {
1700                 $limit_desc .= " $this_limit";
1701             }
1702         }
1703     }
1704     foreach my $k (keys (%group_OR_limits)) {
1705         $limit .= " and " if ( $query || $limit );
1706         $limit .= "($group_OR_limits{$k})";
1707     }
1708     if ($availability_limit) {
1709         $limit .= " and " if ( $query || $limit );
1710         $limit .= "($availability_limit)";
1711     }
1712
1713     # Normalize the query and limit strings
1714     # This is flawed , means we can't search anything with : in it
1715     # if user wants to do ccl or cql, start the query with that
1716 #    $query =~ s/:/=/g;
1717     #NOTE: We use several several different regexps here as you can't have variable length lookback assertions
1718     $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
1719     $query =~ s/(?<=(wrdl)):/=/g;
1720     $query =~ s/(?<=(trn|phr)):/=/g;
1721     $query =~ s/(?<=(st-numeric)):/=/g;
1722     $query =~ s/(?<=(st-year)):/=/g;
1723     $query =~ s/(?<=(st-date-normalized)):/=/g;
1724     $limit =~ s/:/=/g;
1725     for ( $query, $query_desc, $limit, $limit_desc ) {
1726         s/  +/ /g;    # remove extra spaces
1727         s/^ //g;     # remove any beginning spaces
1728         s/ $//g;     # remove any ending spaces
1729         s/==/=/g;    # remove double == from query
1730     }
1731     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1732
1733     for ($query_cgi,$simple_query) {
1734         s/"//g;
1735     }
1736     # append the limit to the query
1737     $query .= " " . $limit;
1738
1739     # Warnings if DEBUG
1740     if ($DEBUG) {
1741         warn "QUERY:" . $query;
1742         warn "QUERY CGI:" . $query_cgi;
1743         warn "QUERY DESC:" . $query_desc;
1744         warn "LIMIT:" . $limit;
1745         warn "LIMIT CGI:" . $limit_cgi;
1746         warn "LIMIT DESC:" . $limit_desc;
1747         warn "---------\nLeave buildQuery\n---------";
1748     }
1749     return (
1750         undef,              $query, $simple_query, $query_cgi,
1751         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1752         $query_type
1753     );
1754 }
1755
1756 =head2 _build_initial_query
1757
1758   ($query, $query_cgi, $query_desc, $previous_operand) = _build_initial_query($initial_query_params);
1759
1760   Build a section of the initial query containing indexes, operators, and operands.
1761
1762 =cut
1763
1764 sub _build_initial_query {
1765     my ($params) = @_;
1766
1767     my $operator = "";
1768     if ($params->{previous_operand}){
1769         #If there is a previous operand, add a supplied operator or the default 'and'
1770         $operator = ($params->{operator}) ? " ".($params->{operator})." " : ' and ';
1771     }
1772
1773     #NOTE: indexes_set is typically set when doing truncation or field weighting
1774     my $operand = ($params->{indexes_set}) ? $params->{parsed_operand} : $params->{index_plus}.$params->{parsed_operand};
1775
1776     #e.g. "kw,wrdl:test"
1777     #e.g. " and kw,wrdl:test"
1778     $params->{query} .= $operator . $operand;
1779
1780     $params->{query_cgi} .= "&op=".uri_escape_utf8($operator) if $operator;
1781     $params->{query_cgi} .= "&idx=".uri_escape_utf8($params->{index}) if $params->{index};
1782     $params->{query_cgi} .= "&q=".uri_escape_utf8($params->{original_operand}) if $params->{original_operand};
1783
1784     #e.g. " and kw,wrdl: test"
1785     $params->{query_desc} .= $operator . $params->{index_plus} . " " . $params->{original_operand};
1786
1787     $params->{previous_operand} = 1 unless $params->{previous_operand}; #If there is no previous operand, mark this as one
1788
1789     return ($params->{query}, $params->{query_cgi}, $params->{query_desc}, $params->{previous_operand});
1790 }
1791
1792 =head2 searchResults
1793
1794   my @search_results = searchResults($search_context, $searchdesc, $hits, 
1795                                      $results_per_page, $offset, $scan, 
1796                                      @marcresults);
1797
1798 Format results in a form suitable for passing to the template
1799
1800 =cut
1801
1802 # IMO this subroutine is pretty messy still -- it's responsible for
1803 # building the HTML output for the template
1804 sub searchResults {
1805     my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
1806     my $dbh = C4::Context->dbh;
1807     my @newresults;
1808
1809     require C4::Items;
1810
1811     $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
1812     my ($is_opac, $hidelostitems);
1813     if ($search_context eq 'opac') {
1814         $hidelostitems = C4::Context->preference('hidelostitems');
1815         $is_opac       = 1;
1816     }
1817
1818     #Build branchnames hash
1819     #find branchname
1820     #get branch information.....
1821     my %branches;
1822     my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1823     $bsth->execute();
1824     while ( my $bdata = $bsth->fetchrow_hashref ) {
1825         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1826     }
1827 # FIXME - We build an authorised values hash here, using the default framework
1828 # though it is possible to have different authvals for different fws.
1829
1830     my $shelflocations =GetKohaAuthorisedValues('items.location','');
1831
1832     # get notforloan authorised value list (see $shelflocations  FIXME)
1833     my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1834
1835     #Get itemtype hash
1836     my %itemtypes = %{ GetItemTypes() };
1837
1838     #search item field code
1839     my ($itemtag, undef) = &GetMarcFromKohaField( "items.itemnumber", "" );
1840
1841     ## find column names of items related to MARC
1842     my %subfieldstosearch;
1843     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1844     for my $column ( @columns ) {
1845         my ( $tagfield, $tagsubfield ) =
1846           &GetMarcFromKohaField( "items." . $column, "" );
1847         if ( defined $tagsubfield ) {
1848             $subfieldstosearch{$column} = $tagsubfield;
1849         }
1850     }
1851
1852     # handle which records to actually retrieve
1853     my $times;
1854     if ( $hits && $offset + $results_per_page <= $hits ) {
1855         $times = $offset + $results_per_page;
1856     }
1857     else {
1858         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1859     }
1860
1861     my $marcflavour = C4::Context->preference("marcflavour");
1862     # We get the biblionumber position in MARC
1863     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1864
1865     # loop through all of the records we've retrieved
1866     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1867
1868         my $marcrecord;
1869         if ($scan) {
1870             # For Scan searches we built USMARC data
1871             $marcrecord = MARC::Record->new_from_usmarc( $marcresults->[$i]);
1872         } else {
1873             # Normal search, render from Zebra's output
1874             $marcrecord = new_record_from_zebra(
1875                 'biblioserver',
1876                 $marcresults->[$i]
1877             );
1878
1879             if ( ! defined $marcrecord ) {
1880                 warn "ERROR DECODING RECORD - $@: " . $marcresults->[$i];
1881                 next;
1882             }
1883         }
1884
1885         my $fw = $scan
1886              ? undef
1887              : $bibliotag < 10
1888                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
1889                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1890
1891         SetUTF8Flag($marcrecord);
1892         my $oldbiblio = TransformMarcToKoha( $marcrecord, $fw );
1893         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1894         $oldbiblio->{result_number} = $i + 1;
1895
1896         # add imageurl to itemtype if there is one
1897         $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1898
1899         $oldbiblio->{'authorised_value_images'}  = ($search_context eq 'opac' && C4::Context->preference('AuthorisedValueImages')) || ($search_context eq 'intranet' && C4::Context->preference('StaffAuthorisedValueImages')) ? C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) ) : [];
1900                 $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
1901                 $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
1902                 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1903                 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1904                 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1905
1906                 # edition information, if any
1907         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1908         $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{translated_description};
1909  # Build summary if there is one (the summary is defined in the itemtypes table)
1910  # FIXME: is this used anywhere, I think it can be commented out? -- JF
1911         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1912             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1913             my @fields  = $marcrecord->fields();
1914
1915             my $newsummary;
1916             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1917                 my $tags = {};
1918                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1919                     $tag =~ /(.{3})(.)/;
1920                     if($marcrecord->field($1)){
1921                         my @abc = $marcrecord->field($1)->subfield($2);
1922                         $tags->{$tag} = $#abc + 1 ;
1923                     }
1924                 }
1925
1926                 # We catch how many times to repeat this line
1927                 my $max = 0;
1928                 foreach my $tag (keys(%$tags)){
1929                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1930                  }
1931
1932                 # we replace, and repeat each line
1933                 for (my $i = 0 ; $i < $max ; $i++){
1934                     my $newline = $line;
1935
1936                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1937                         $tag =~ /(.{3})(.)/;
1938
1939                         if($marcrecord->field($1)){
1940                             my @repl = $marcrecord->field($1)->subfield($2);
1941                             my $subfieldvalue = $repl[$i];
1942                             $newline =~ s/\[$tag\]/$subfieldvalue/g;
1943                         }
1944                     }
1945                     $newsummary .= "$newline\n";
1946                 }
1947             }
1948
1949             $newsummary =~ s/\[(.*?)]//g;
1950             $newsummary =~ s/\n/<br\/>/g;
1951             $oldbiblio->{summary} = $newsummary;
1952         }
1953
1954         # Pull out the items fields
1955         my @fields = $marcrecord->field($itemtag);
1956         my $marcflavor = C4::Context->preference("marcflavour");
1957         # adding linked items that belong to host records
1958         my $analyticsfield = '773';
1959         if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
1960             $analyticsfield = '773';
1961         } elsif ($marcflavor eq 'UNIMARC') {
1962             $analyticsfield = '461';
1963         }
1964         foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
1965             my $hostbiblionumber = $hostfield->subfield("0");
1966             my $linkeditemnumber = $hostfield->subfield("9");
1967             if(!$hostbiblionumber eq undef){
1968                 my $hostbiblio = GetMarcBiblio($hostbiblionumber, 1);
1969                 my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
1970                 if(!$hostbiblio eq undef){
1971                     my @hostitems = $hostbiblio->field($itemfield);
1972                     foreach my $hostitem (@hostitems){
1973                         if ($hostitem->subfield("9") eq $linkeditemnumber){
1974                             my $linkeditem =$hostitem;
1975                             # append linked items if they exist
1976                             if (!$linkeditem eq undef){
1977                                 push (@fields, $linkeditem);}
1978                         }
1979                     }
1980                 }
1981             }
1982         }
1983
1984         # Setting item statuses for display
1985         my @available_items_loop;
1986         my @onloan_items_loop;
1987         my @other_items_loop;
1988
1989         my $available_items;
1990         my $onloan_items;
1991         my $other_items;
1992
1993         my $ordered_count         = 0;
1994         my $available_count       = 0;
1995         my $onloan_count          = 0;
1996         my $longoverdue_count     = 0;
1997         my $other_count           = 0;
1998         my $withdrawn_count        = 0;
1999         my $itemlost_count        = 0;
2000         my $hideatopac_count      = 0;
2001         my $itembinding_count     = 0;
2002         my $itemdamaged_count     = 0;
2003         my $item_in_transit_count = 0;
2004         my $can_place_holds       = 0;
2005         my $item_onhold_count     = 0;
2006         my $notforloan_count      = 0;
2007         my $items_count           = scalar(@fields);
2008         my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
2009         my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
2010         my @hiddenitems; # hidden itemnumbers based on OpacHiddenItems syspref
2011
2012         # loop through every item
2013         foreach my $field (@fields) {
2014             my $item;
2015
2016             # populate the items hash
2017             foreach my $code ( keys %subfieldstosearch ) {
2018                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
2019             }
2020             $item->{description} = $itemtypes{ $item->{itype} }{translated_description};
2021
2022                 # OPAC hidden items
2023             if ($is_opac) {
2024                 # hidden because lost
2025                 if ($hidelostitems && $item->{itemlost}) {
2026                     $hideatopac_count++;
2027                     next;
2028                 }
2029                 # hidden based on OpacHiddenItems syspref
2030                 my @hi = C4::Items::GetHiddenItemnumbers($item);
2031                 if (scalar @hi) {
2032                     push @hiddenitems, @hi;
2033                     $hideatopac_count++;
2034                     next;
2035                 }
2036             }
2037
2038             my $hbranch     = C4::Context->preference('StaffSearchResultsDisplayBranch');
2039             my $otherbranch = $hbranch eq 'homebranch' ? 'holdingbranch' : 'homebranch';
2040
2041             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
2042             if ($item->{$hbranch}) {
2043                 $item->{'branchname'} = $branches{$item->{$hbranch}};
2044             }
2045             elsif ($item->{$otherbranch}) {     # Last resort
2046                 $item->{'branchname'} = $branches{$item->{$otherbranch}};
2047             }
2048
2049                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
2050 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
2051             my $userenv = C4::Context->userenv;
2052             if ( $item->{onloan}
2053                 && !( C4::Members::GetHideLostItemsPreference( $userenv->{'number'} ) && $item->{itemlost} ) )
2054             {
2055                 $onloan_count++;
2056                 my $key = $prefix . $item->{onloan} . $item->{barcode};
2057                 $onloan_items->{$key}->{due_date} = output_pref( { dt => dt_from_string( $item->{onloan} ), dateonly => 1 } );
2058                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
2059                 $onloan_items->{$key}->{branchname}     = $item->{branchname};
2060                 $onloan_items->{$key}->{location}       = $shelflocations->{ $item->{location} };
2061                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
2062                 $onloan_items->{$key}->{description}    = $item->{description};
2063                 $onloan_items->{$key}->{imageurl} =
2064                   getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
2065
2066                 # if something's checked out and lost, mark it as 'long overdue'
2067                 if ( $item->{itemlost} ) {
2068                     $onloan_items->{$key}->{longoverdue}++;
2069                     $longoverdue_count++;
2070                 }
2071                 else {    # can place holds as long as item isn't lost
2072                     $can_place_holds = 1;
2073                 }
2074             }
2075
2076          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
2077             else {
2078
2079                 $item->{notforloan}=1 if !$item->{notforloan}  && $itemtypes{ C4::Context->preference("item-level_itypes")? $item->{itype}: $oldbiblio->{itemtype} }->{notforloan};
2080
2081                 # item is on order
2082                 if ( $item->{notforloan} < 0 ) {
2083                     $ordered_count++;
2084                 } elsif ( $item->{notforloan} > 0 ) {
2085                     $notforloan_count++;
2086                 }
2087
2088                 # is item in transit?
2089                 my $transfertwhen = '';
2090                 my ($transfertfrom, $transfertto);
2091
2092                 # is item on the reserve shelf?
2093                 my $reservestatus = '';
2094
2095                 unless ($item->{withdrawn}
2096                         || $item->{itemlost}
2097                         || $item->{damaged}
2098                         || $item->{notforloan}
2099                         || ( C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck')
2100                         && $items_count > C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck') ) ) {
2101
2102                     # A couple heuristics to limit how many times
2103                     # we query the database for item transfer information, sacrificing
2104                     # accuracy in some cases for speed;
2105                     #
2106                     # 1. don't query if item has one of the other statuses
2107                     # 2. don't check transit status if the bib has
2108                     #    more than 20 items
2109                     #
2110                     # FIXME: to avoid having the query the database like this, and to make
2111                     #        the in transit status count as unavailable for search limiting,
2112                     #        should map transit status to record indexed in Zebra.
2113                     #
2114                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
2115                     $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
2116                 }
2117
2118                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
2119                 if (   $item->{withdrawn}
2120                     || $item->{itemlost}
2121                     || $item->{damaged}
2122                     || $item->{notforloan}
2123                     || $reservestatus eq 'Waiting'
2124                     || ($transfertwhen ne ''))
2125                 {
2126                     $withdrawn_count++        if $item->{withdrawn};
2127                     $itemlost_count++        if $item->{itemlost};
2128                     $itemdamaged_count++     if $item->{damaged};
2129                     $item_in_transit_count++ if $transfertwhen ne '';
2130                     $item_onhold_count++     if $reservestatus eq 'Waiting';
2131                     $item->{status} = $item->{withdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
2132
2133                     # can place a hold on a item if
2134                     # not lost nor withdrawn
2135                     # not damaged unless AllowHoldsOnDamagedItems is true
2136                     # item is either for loan or on order (notforloan < 0)
2137                     $can_place_holds = 1
2138                       if (
2139                            !$item->{itemlost}
2140                         && !$item->{withdrawn}
2141                         && ( !$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems') )
2142                         && ( !$item->{notforloan} || $item->{notforloan} < 0 )
2143                       );
2144
2145                     $other_count++;
2146
2147                     my $key = $prefix . $item->{status};
2148                     foreach (qw(withdrawn itemlost damaged branchname itemcallnumber)) {
2149                         $other_items->{$key}->{$_} = $item->{$_};
2150                     }
2151                     $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
2152                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
2153                     $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
2154                                         $other_items->{$key}->{count}++ if $item->{$hbranch};
2155                                         $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
2156                                         $other_items->{$key}->{description} = $item->{description};
2157                                         $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
2158                 }
2159                 # item is available
2160                 else {
2161                     $can_place_holds = 1;
2162                     $available_count++;
2163                                         $available_items->{$prefix}->{count}++ if $item->{$hbranch};
2164                                         foreach (qw(branchname itemcallnumber description)) {
2165                         $available_items->{$prefix}->{$_} = $item->{$_};
2166                                         }
2167                                         $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
2168                                         $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
2169                 }
2170             }
2171         }    # notforloan, item level and biblioitem level
2172
2173         # if all items are hidden, do not show the record
2174         if ($items_count > 0 && $hideatopac_count == $items_count) {
2175             next;
2176         }
2177
2178         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
2179         for my $key ( sort keys %$onloan_items ) {
2180             (++$onloanitemscount > $maxitems) and last;
2181             push @onloan_items_loop, $onloan_items->{$key};
2182         }
2183         for my $key ( sort keys %$other_items ) {
2184             (++$otheritemscount > $maxitems) and last;
2185             push @other_items_loop, $other_items->{$key};
2186         }
2187         for my $key ( sort keys %$available_items ) {
2188             (++$availableitemscount > $maxitems) and last;
2189             push @available_items_loop, $available_items->{$key}
2190         }
2191
2192         # XSLT processing of some stuff
2193         my $interface = $search_context eq 'opac' ? 'OPAC' : '';
2194         if (!$scan && C4::Context->preference($interface . "XSLTResultsDisplay")) {
2195             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, $interface."XSLTResultsDisplay", 1, \@hiddenitems);
2196         # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
2197         }
2198
2199         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
2200         if (!C4::Context->preference("item-level_itypes")) {
2201             if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
2202                 $can_place_holds = 0;
2203             }
2204         }
2205         $oldbiblio->{norequests} = 1 unless $can_place_holds;
2206         $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
2207         $oldbiblio->{items_count}          = $items_count;
2208         $oldbiblio->{available_items_loop} = \@available_items_loop;
2209         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
2210         $oldbiblio->{other_items_loop}     = \@other_items_loop;
2211         $oldbiblio->{availablecount}       = $available_count;
2212         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
2213         $oldbiblio->{onloancount}          = $onloan_count;
2214         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
2215         $oldbiblio->{othercount}           = $other_count;
2216         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
2217         $oldbiblio->{withdrawncount}        = $withdrawn_count;
2218         $oldbiblio->{itemlostcount}        = $itemlost_count;
2219         $oldbiblio->{damagedcount}         = $itemdamaged_count;
2220         $oldbiblio->{intransitcount}       = $item_in_transit_count;
2221         $oldbiblio->{onholdcount}          = $item_onhold_count;
2222         $oldbiblio->{orderedcount}         = $ordered_count;
2223         $oldbiblio->{notforloancount}      = $notforloan_count;
2224
2225         if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
2226             my $fieldspec = C4::Context->preference("AlternateHoldingsField");
2227             my $subfields = substr $fieldspec, 3;
2228             my $holdingsep = C4::Context->preference("AlternateHoldingsSeparator") || ' ';
2229             my @alternateholdingsinfo = ();
2230             my @holdingsfields = $marcrecord->field(substr $fieldspec, 0, 3);
2231             my $alternateholdingscount = 0;
2232
2233             for my $field (@holdingsfields) {
2234                 my %holding = ( holding => '' );
2235                 my $havesubfield = 0;
2236                 for my $subfield ($field->subfields()) {
2237                     if ((index $subfields, $$subfield[0]) >= 0) {
2238                         $holding{'holding'} .= $holdingsep if (length $holding{'holding'} > 0);
2239                         $holding{'holding'} .= $$subfield[1];
2240                         $havesubfield++;
2241                     }
2242                 }
2243                 if ($havesubfield) {
2244                     push(@alternateholdingsinfo, \%holding);
2245                     $alternateholdingscount++;
2246                 }
2247             }
2248
2249             $oldbiblio->{'ALTERNATEHOLDINGS'} = \@alternateholdingsinfo;
2250             $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
2251         }
2252
2253         push( @newresults, $oldbiblio );
2254     }
2255
2256     return @newresults;
2257 }
2258
2259 =head2 SearchAcquisitions
2260     Search for acquisitions
2261 =cut
2262
2263 sub SearchAcquisitions{
2264     my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
2265
2266     my $dbh=C4::Context->dbh;
2267     # Variable initialization
2268     my $str=qq|
2269     SELECT marcxml
2270     FROM biblio
2271     LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
2272     LEFT JOIN items ON items.biblionumber=biblio.biblionumber
2273     WHERE dateaccessioned BETWEEN ? AND ?
2274     |;
2275
2276     my (@params,@loopcriteria);
2277
2278     push @params, $datebegin->output("iso");
2279     push @params, $dateend->output("iso");
2280
2281     if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
2282         if(C4::Context->preference("item-level_itypes")){
2283             $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
2284         }else{
2285             $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
2286         }
2287         push @params, @$itemtypes;
2288     }
2289
2290     if ($criteria =~/itemtype/){
2291         if(C4::Context->preference("item-level_itypes")){
2292             $str .= "AND items.itype=? ";
2293         }else{
2294             $str .= "AND biblioitems.itemtype=? ";
2295         }
2296
2297         if(scalar(@$itemtypes) == 0){
2298             my $itypes = GetItemTypes();
2299             for my $key (keys %$itypes){
2300                 push @$itemtypes, $key;
2301             }
2302         }
2303
2304         @loopcriteria= @$itemtypes;
2305     }elsif ($criteria=~/itemcallnumber/){
2306         $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
2307                  OR items.itemcallnumber is NULL
2308                  OR items.itemcallnumber = '')";
2309
2310         @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
2311     }else {
2312         $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
2313         @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
2314     }
2315
2316     if ($orderby =~ /date_desc/){
2317         $str.=" ORDER BY dateaccessioned DESC";
2318     } else {
2319         $str.=" ORDER BY title";
2320     }
2321
2322     my $qdataacquisitions=$dbh->prepare($str);
2323
2324     my @loopacquisitions;
2325     foreach my $value(@loopcriteria){
2326         push @params,$value;
2327         my %cell;
2328         $cell{"title"}=$value;
2329         $cell{"titlecode"}=$value;
2330
2331         eval{$qdataacquisitions->execute(@params);};
2332
2333         if ($@){ warn "recentacquisitions Error :$@";}
2334         else {
2335             my @loopdata;
2336             while (my $data=$qdataacquisitions->fetchrow_hashref){
2337                 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
2338             }
2339             $cell{"loopdata"}=\@loopdata;
2340         }
2341         push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
2342         pop @params;
2343     }
2344     $qdataacquisitions->finish;
2345     return \@loopacquisitions;
2346 }
2347
2348 =head2 enabled_staff_search_views
2349
2350 %hash = enabled_staff_search_views()
2351
2352 This function returns a hash that contains three flags obtained from the system
2353 preferences, used to determine whether a particular staff search results view
2354 is enabled.
2355
2356 =over 2
2357
2358 =item C<Output arg:>
2359
2360     * $hash{can_view_MARC} is true only if the MARC view is enabled
2361     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2362     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2363
2364 =item C<usage in the script:>
2365
2366 =back
2367
2368 $template->param ( C4::Search::enabled_staff_search_views );
2369
2370 =cut
2371
2372 sub enabled_staff_search_views
2373 {
2374         return (
2375                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2376                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2377                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2378         );
2379 }
2380
2381 =head2 z3950_search_args
2382
2383 $arrayref = z3950_search_args($matchpoints)
2384
2385 This function returns an array reference that contains the search parameters to be
2386 passed to the Z39.50 search script (z3950_search.pl). The array elements
2387 are hash refs whose keys are name and value, and whose values are the
2388 name of a search parameter, the value of that search parameter and the URL encoded
2389 value of that parameter.
2390
2391 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2392
2393 The search parameter values are obtained from the bibliographic record whose
2394 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2395
2396 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2397 a general purpose search argument. In this case, the returned array contains only
2398 entry: the key is 'title' and the value is derived from $matchpoints.
2399
2400 If a search parameter value is undefined or empty, it is not included in the returned
2401 array.
2402
2403 The returned array reference may be passed directly to the template parameters.
2404
2405 =over 2
2406
2407 =item C<Output arg:>
2408
2409     * $array containing hash refs as described above
2410
2411 =item C<usage in the script:>
2412
2413 =back
2414
2415 $data = Biblio::GetBiblioData($bibno);
2416 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2417
2418 *OR*
2419
2420 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2421
2422 =cut
2423
2424 sub z3950_search_args {
2425     my $bibrec = shift;
2426
2427     my $isbn_string = ref( $bibrec ) ? $bibrec->{title} : $bibrec;
2428     my $isbn = Business::ISBN->new( $isbn_string );
2429
2430     if (defined $isbn && $isbn->is_valid)
2431     {
2432         if ( ref($bibrec) ) {
2433             $bibrec->{isbn} = $isbn_string;
2434             $bibrec->{title} = undef;
2435         } else {
2436             $bibrec = { isbn => $isbn_string };
2437         }
2438     }
2439     else {
2440         $bibrec = { title => $bibrec } if !ref $bibrec;
2441     }
2442     my $array = [];
2443     for my $field (qw/ lccn isbn issn title author dewey subject /)
2444     {
2445         push @$array, { name => $field, value => $bibrec->{$field} }
2446           if defined $bibrec->{$field};
2447     }
2448     return $array;
2449 }
2450
2451 =head2 GetDistinctValues($field);
2452
2453 C<$field> is a reference to the fields array
2454
2455 =cut
2456
2457 sub GetDistinctValues {
2458     my ($fieldname,$string)=@_;
2459     # returns a reference to a hash of references to branches...
2460     if ($fieldname=~/\./){
2461                         my ($table,$column)=split /\./, $fieldname;
2462                         my $dbh = C4::Context->dbh;
2463                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2464                         my $sth = $dbh->prepare("select DISTINCT($column) as value, count(*) as cnt from $table ".($string?" where $column like \"$string%\"":"")."group by value order by $column ");
2465                         $sth->execute;
2466                         my $elements=$sth->fetchall_arrayref({});
2467                         return $elements;
2468    }
2469    else {
2470                 $string||= qq("");
2471                 my @servers=qw<biblioserver authorityserver>;
2472                 my (@zconns,@results);
2473         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2474                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2475                         $results[$i] =
2476                       $zconns[$i]->scan(
2477                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2478                       );
2479                 }
2480                 # The big moment: asynchronously retrieve results from all servers
2481                 my @elements;
2482         _ZOOM_event_loop(
2483             \@zconns,
2484             \@results,
2485             sub {
2486                 my ( $i, $size ) = @_;
2487                 for ( my $j = 0 ; $j < $size ; $j++ ) {
2488                     my %hashscan;
2489                     @hashscan{qw(value cnt)} =
2490                       $results[ $i - 1 ]->display_term($j);
2491                     push @elements, \%hashscan;
2492                 }
2493             }
2494         );
2495                 return \@elements;
2496    }
2497 }
2498
2499 =head2 _ZOOM_event_loop
2500
2501     _ZOOM_event_loop(\@zconns, \@results, sub {
2502         my ( $i, $size ) = @_;
2503         ....
2504     } );
2505
2506 Processes a ZOOM event loop and passes control to a closure for
2507 processing the results, and destroying the resultsets.
2508
2509 =cut
2510
2511 sub _ZOOM_event_loop {
2512     my ($zconns, $results, $callback) = @_;
2513     while ( ( my $i = ZOOM::event( $zconns ) ) != 0 ) {
2514         my $ev = $zconns->[ $i - 1 ]->last_event();
2515         if ( $ev == ZOOM::Event::ZEND ) {
2516             next unless $results->[ $i - 1 ];
2517             my $size = $results->[ $i - 1 ]->size();
2518             if ( $size > 0 ) {
2519                 $callback->($i, $size);
2520             }
2521         }
2522     }
2523
2524     foreach my $result (@$results) {
2525         $result->destroy();
2526     }
2527 }
2528
2529 =head2 new_record_from_zebra
2530
2531 Given raw data from a Zebra result set, return a MARC::Record object
2532
2533 This helper function is needed to take into account all the involved
2534 system preferences and configuration variables to properly create the
2535 MARC::Record object.
2536
2537 If we are using GRS-1, then the raw data we get from Zebra should be USMARC
2538 data. If we are using DOM, then it has to be MARCXML.
2539
2540 =cut
2541
2542 sub new_record_from_zebra {
2543
2544     my $server   = shift;
2545     my $raw_data = shift;
2546     # Set the default indexing modes
2547     my $index_mode = ( $server eq 'biblioserver' )
2548                         ? C4::Context->config('zebra_bib_index_mode') // 'dom'
2549                         : C4::Context->config('zebra_auth_index_mode') // 'dom';
2550
2551     my $marc_record =  eval {
2552         if ( $index_mode eq 'dom' ) {
2553             MARC::Record->new_from_xml( $raw_data, 'UTF-8' );
2554         } else {
2555             MARC::Record->new_from_usmarc( $raw_data );
2556         }
2557     };
2558
2559     if ($@) {
2560         return;
2561     } else {
2562         return $marc_record;
2563     }
2564
2565 }
2566
2567 END { }    # module clean-up code here (global destructor)
2568
2569 1;
2570 __END__
2571
2572 =head1 AUTHOR
2573
2574 Koha Development Team <http://koha-community.org/>
2575
2576 =cut