a348336bc58f545a3f6a6a4ef9dc3de7f930b417
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / WWW / SuperCat.pm
1 package OpenILS::WWW::SuperCat;
2 use strict; use warnings;
3
4 use Apache2::Log;
5 use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log);
6 use APR::Const    -compile => qw(:error SUCCESS);
7 use Apache2::RequestRec ();
8 use Apache2::RequestIO ();
9 use Apache2::RequestUtil;
10 use CGI;
11 use Data::Dumper;
12 use SRU::Request;
13 use SRU::Response;
14
15 use OpenSRF::EX qw(:try);
16 use OpenSRF::Utils qw/:datetime/;
17 use OpenSRF::Utils::Cache;
18 use OpenSRF::System;
19 use OpenSRF::AppSession;
20 use XML::LibXML;
21 use XML::LibXSLT;
22
23 use Encode;
24 use Unicode::Normalize;
25 use OpenILS::Utils::Fieldmapper;
26 use OpenILS::WWW::SuperCat::Feed;
27 use OpenSRF::Utils::Logger qw/$logger/;
28
29 use MARC::Record;
30 use MARC::File::XML;
31
32 my $log = 'OpenSRF::Utils::Logger';
33
34 # set the bootstrap config when this module is loaded
35 my ($bootstrap, $cstore, $supercat, $actor, $parser, $search, $xslt, $cn_browse_xslt, %browse_types);
36
37 $browse_types{call_number}{xml} = sub {
38         my $tree = shift;
39
40         my $year = (gmtime())[5] + 1900;
41         my $content = '';
42
43         $content .= "<hold:volumes  xmlns:hold='http://open-ils.org/spec/holdings/v1'>";
44
45         for my $cn (@$tree) {
46                 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
47                 $cn_class =~ s/Fieldmapper-//gso;
48
49                 my $cn_tag = "tag:open-ils.org,$year:$cn_class/".$cn->id;
50                 my $cn_lib = $cn->owning_lib->shortname;
51                 my $cn_label = $cn->label;
52
53                 $cn_label =~ s/\n//gos;
54                 $cn_label =~ s/'/&apos;/go;
55
56                 (my $ou_class = $cn->owning_lib->class_name) =~ s/::/-/gso;
57                 $ou_class =~ s/Fieldmapper-//gso;
58
59                 my $ou_tag = "tag:open-ils.org,$year:$ou_class/".$cn->owning_lib->id;
60                 my $ou_name = $cn->owning_lib->name;
61
62                 $ou_name =~ s/\n//gos;
63                 $ou_name =~ s/'/&apos;/go;
64
65                 (my $rec_class = $cn->record->class_name) =~ s/::/-/gso;
66                 $rec_class =~ s/Fieldmapper-//gso;
67
68                 my $rec_tag = "tag:open-ils.org,$year:$rec_class/".$cn->record->id.'/'.$cn->owning_lib->shortname;
69
70                 $content .= "<hold:volume id='$cn_tag' lib='$cn_lib' label='$cn_label'>";
71                 $content .= "<act:owning_lib xmlns:act='http://open-ils.org/spec/actors/v1' id='$ou_tag' name='$ou_name'/>";
72
73                 my $r_doc = $parser->parse_string($cn->record->marc);
74                 $r_doc->documentElement->setAttribute( id => $rec_tag );
75                 $content .= entityize($r_doc->documentElement->toString);
76
77                 $content .= "</hold:volume>";
78         }
79
80         $content .= '</hold:volumes>';
81         return ("Content-type: application/xml\n\n",$content);
82 };
83
84
85 $browse_types{call_number}{html} = sub {
86         my $tree = shift;
87         my $p = shift;
88         my $n = shift;
89
90         if (!$cn_browse_xslt) {
91                 $cn_browse_xslt = $parser->parse_file(
92                         OpenSRF::Utils::SettingsClient
93                                 ->new
94                                 ->config_value( dirs => 'xsl' ).
95                         "/CNBrowse2HTML.xsl"
96                 );
97                 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
98         }
99
100         my (undef,$xml) = $browse_types{call_number}{xml}->($tree);
101
102         return (
103                 "Content-type: text/html\n\n",
104                 entityize(
105                         $cn_browse_xslt->transform(
106                                 $parser->parse_string( $xml ),
107                                 'prev' => "'$p'",
108                                 'next' => "'$n'"
109                         )->toString(1)
110                 )
111         );
112 };
113
114 sub import {
115         my $self = shift;
116         $bootstrap = shift;
117 }
118
119
120 sub child_init {
121         OpenSRF::System->bootstrap_client( config_file => $bootstrap );
122         
123         my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
124         Fieldmapper->import(IDL => $idl);
125
126         $supercat = OpenSRF::AppSession->create('open-ils.supercat');
127         $cstore = OpenSRF::AppSession->create('open-ils.cstore');
128         $actor = OpenSRF::AppSession->create('open-ils.actor');
129         $search = OpenSRF::AppSession->create('open-ils.search');
130         $parser = new XML::LibXML;
131         $xslt = new XML::LibXSLT;
132
133         $cn_browse_xslt = $parser->parse_file(
134                 OpenSRF::Utils::SettingsClient
135                         ->new
136                         ->config_value( dirs => 'xsl' ).
137                 "/CNBrowse2HTML.xsl"
138         );
139
140         $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
141
142 }
143
144 sub oisbn {
145
146         my $apache = shift;
147         return Apache2::Const::DECLINED if (-e $apache->filename);
148
149         (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
150
151         my $list = $supercat
152                 ->request("open-ils.supercat.oisbn", $isbn)
153                 ->gather(1);
154
155         print "Content-type: application/xml; charset=utf-8\n\n";
156         print "<?xml version='1.0' encoding='UTF-8' ?>\n";
157
158         unless (exists $$list{metarecord}) {
159                 print '<idlist/>';
160                 return Apache2::Const::OK;
161         }
162
163         print "<idlist metarecord='$$list{metarecord}'>\n";
164
165         for ( keys %{ $$list{record_list} } ) {
166                 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
167                 print "  <isbn record='$_'>$o</isbn>\n"
168         }
169
170         print "</idlist>\n";
171
172         return Apache2::Const::OK;
173 }
174
175 sub unapi {
176
177         my $apache = shift;
178         return Apache2::Const::DECLINED if (-e $apache->filename);
179
180         my $cgi = new CGI;
181
182         my $add_path = 0;
183         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
184                 my $rel_name = $cgi->url(-relative=>1);
185                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
186         }
187
188         my $url = $cgi->url(-path_info=>$add_path);
189         my $root = (split 'unapi', $url)[0];
190         my $base = (split 'unapi', $url)[0] . 'unapi';
191
192
193         my $uri = $cgi->param('id') || '';
194         my $host = $cgi->virtual_host || $cgi->server_name;
195
196         my $format = $cgi->param('format');
197         my $flesh_feed = ($format =~ /-full$/o) ? 1 : 0;
198         (my $base_format = $format) =~ s/-full$//o;
199         my ($id,$type,$command,$lib) = ('','','');
200
201         if (!$format) {
202                 my $body = "Content-type: application/xml; charset=utf-8\n\n";
203         
204                 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^/]+)(?:/(.+))$}o) {
205                         $id = $2;
206                         $lib = uc($3);
207                         $type = 'record';
208                         $type = 'metarecord' if ($1 =~ /^m/o);
209
210                         my $list = $supercat
211                                 ->request("open-ils.supercat.$type.formats")
212                                 ->gather(1);
213
214                         if ($type eq 'record' or $type eq 'isbn') {
215                                 $body .= <<"                            FORMATS";
216 <formats id='$uri'>
217         <format name='opac' type='text/html'/>
218         <format name='html' type='text/html'/>
219         <format name='htmlholdings' type='text/html'/>
220         <format name='html-full' type='text/html'/>
221         <format name='htmlholdings-full' type='text/html'/>
222                                 FORMATS
223                         } elsif ($type eq 'metarecord') {
224                                 $body .= <<"                            FORMATS";
225                                 <formats id='$uri'>
226                                         <format name='opac' type='text/html'/>
227                                 FORMATS
228                         }
229
230                         for my $h (@$list) {
231                                 my ($type) = keys %$h;
232                                 $body .= "\t<format name='$type' type='application/xml'";
233
234                                 for my $part ( qw/namespace_uri docs schema_location/ ) {
235                                         $body .= " $part='$$h{$type}{$part}'"
236                                                 if ($$h{$type}{$part});
237                                 }
238                                 
239                                 $body .= "/>\n";
240
241                                 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
242                                         $body .= "\t<format name='$type-full' type='application/xml'";
243
244                                         for my $part ( qw/namespace_uri docs schema_location/ ) {
245                                                 $body .= " $part='$$h{$type}{$part}'"
246                                                         if ($$h{$type}{$part});
247                                         }
248                                 
249                                         $body .= "/>\n";
250                                 }
251                         }
252
253                         $body .= "</formats>\n";
254
255                 } else {
256                         my $list = $supercat
257                                 ->request("open-ils.supercat.record.formats")
258                                 ->gather(1);
259                                 
260                         push @$list,
261                                 @{ $supercat
262                                         ->request("open-ils.supercat.metarecord.formats")
263                                         ->gather(1);
264                                 };
265
266                         my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
267                         $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
268
269                         $body .= <<"                    FORMATS";
270 <formats>
271         <format name='opac' type='text/html'/>
272         <format name='html' type='text/html'/>
273         <format name='htmlholdings' type='text/html'/>
274         <format name='html-full' type='text/html'/>
275         <format name='htmlholdings-full' type='text/html'/>
276                         FORMATS
277
278
279                         for my $h (@$list) {
280                                 my ($type) = keys %$h;
281                                 $body .= "\t<format name='$type' type='application/xml'";
282
283                                 for my $part ( qw/namespace_uri docs schema_location/ ) {
284                                         $body .= " $part='$$h{$type}{$part}'"
285                                                 if ($$h{$type}{$part});
286                                 }
287                                 
288                                 $body .= "/>\n";
289
290                                 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
291                                         $body .= "\t<format name='$type-full' type='application/xml'";
292
293                                         for my $part ( qw/namespace_uri docs schema_location/ ) {
294                                                 $body .= " $part='$$h{$type}{$part}'"
295                                                         if ($$h{$type}{$part});
296                                         }
297                                 
298                                         $body .= "/>\n";
299                                 }
300                         }
301
302                         $body .= "</formats>\n";
303
304                 }
305                 print $body;
306                 return Apache2::Const::OK;
307         }
308
309         if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^/]+)(?:/(.+))?}o) {
310                 $id = $2;
311                 $lib = uc($3);
312                 $type = 'record';
313                 $type = 'metarecord' if ($1 =~ /^metabib/o);
314                 $type = 'isbn' if ($1 =~ /^isbn/o);
315                 $type = 'call_number' if ($1 =~ /^call_number/o);
316                 $command = 'retrieve';
317                 $command = 'browse' if ($type eq 'call_number');
318         }
319
320         if (!$lib || $lib eq '-') {
321                 $lib = $actor->request(
322                         'open-ils.actor.org_unit_list.search' => parent_ou => undef
323                 )->gather(1)->[0]->shortname;
324         }
325
326         my $lib_object = $actor->request(
327                 'open-ils.actor.org_unit_list.search' => shortname => $lib
328         )->gather(1)->[0];
329         my $lib_id = $lib_object->id;
330
331         my $ou_types = $actor->request( 'open-ils.actor.org_types.retrieve' )->gather(1);
332         my $lib_depth = (grep { $_->id == $lib_object->ou_type } @$ou_types)[0]->depth;
333
334         if ($type eq 'call_number' and $command eq 'browse') {
335                 print "Location: $root/browse/$base_format/call_number/$lib/$id\n\n";
336                 return 302;
337         }
338
339         if ($type eq 'isbn') {
340                 my $rec = $supercat->request('open-ils.supercat.isbn.object.retrieve',$id)->gather(1);
341                 if (!@$rec) {
342                         print "Content-type: text/html; charset=utf-8\n\n";
343                         $apache->custom_response( 404, <<"                      HTML");
344                         <html>
345                                 <head>
346                                         <title>Type [$type] with id [$id] not found!</title>
347                                 </head>
348                                 <body>
349                                         <br/>
350                                         <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
351                                 </body>
352                         </html>
353                         HTML
354                         return 404;
355                 }
356                 $id = $rec->[0]->id;
357                 $type = 'record';
358         }
359
360         if ( !grep
361                { (keys(%$_))[0] eq $base_format }
362                @{ $supercat->request("open-ils.supercat.$type.formats")->gather(1) }
363              and !grep
364                { $_ eq $base_format }
365                qw/opac html htmlholdings/
366         ) {
367                 print "Content-type: text/html; charset=utf-8\n\n";
368                 $apache->custom_response( 406, <<"              HTML");
369                 <html>
370                         <head>
371                                 <title>Invalid format [$format] for type [$type]!</title>
372                         </head>
373                         <body>
374                                 <br/>
375                                 <center>Sorry, format $format is not valid for type $type.</center>
376                         </body>
377                 </html>
378                 HTML
379                 return 406;
380         }
381
382         if ($format eq 'opac') {
383                 print "Location: $root/../../en-US/skin/default/xml/rresult.xml?m=$id&l=$lib_id&d=$lib_depth\n\n"
384                         if ($type eq 'metarecord');
385                 print "Location: $root/../../en-US/skin/default/xml/rdetail.xml?r=$id&l=$lib_id&d=$lib_depth\n\n"
386                         if ($type eq 'record');
387                 return 302;
388         } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
389                 my $feed = create_record_feed(
390                         $type,
391                         $format => [ $id ],
392                         $base,
393                         $lib,
394                         $flesh_feed
395                 );
396
397                 if (!$feed->count) {
398                         print "Content-type: text/html; charset=utf-8\n\n";
399                         $apache->custom_response( 404, <<"                      HTML");
400                         <html>
401                                 <head>
402                                         <title>Type [$type] with id [$id] not found!</title>
403                                 </head>
404                                 <body>
405                                         <br/>
406                                         <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
407                                 </body>
408                         </html>
409                         HTML
410                         return 404;
411                 }
412
413                 $feed->root($root);
414                 $feed->creator($host);
415                 $feed->update_ts(gmtime_ISO8601());
416                 $feed->link( unapi => $base) if ($flesh_feed);
417
418                 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
419                 print entityize($feed->toString) . "\n";
420
421                 return Apache2::Const::OK;
422         }
423
424         my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
425         my $data = $req->gather(1);
426
427         if ($req->failed || !$data) {
428                 print "Content-type: text/html; charset=utf-8\n\n";
429                 $apache->custom_response( 404, <<"              HTML");
430                 <html>
431                         <head>
432                                 <title>$type $id not found!</title>
433                         </head>
434                         <body>
435                                 <br/>
436                                 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
437                         </body>
438                 </html>
439                 HTML
440                 return 404;
441         }
442
443         print "Content-type: application/xml; charset=utf-8\n\n$data";
444
445         return Apache2::Const::OK;
446 }
447
448 sub supercat {
449
450         my $apache = shift;
451         return Apache2::Const::DECLINED if (-e $apache->filename);
452
453         my $cgi = new CGI;
454
455         my $add_path = 0;
456         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
457                 my $rel_name = $cgi->url(-relative=>1);
458                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
459         }
460
461         my $url = $cgi->url(-path_info=>$add_path);
462         my $root = (split 'supercat', $url)[0];
463         my $base = (split 'supercat', $url)[0] . 'supercat';
464         my $unapi = (split 'supercat', $url)[0] . 'unapi';
465
466         my $host = $cgi->virtual_host || $cgi->server_name;
467
468         my $path = $cgi->path_info;
469         my ($id,$type,$format,$command) = reverse split '/', $path;
470         my $flesh_feed = ($type =~ /-full$/o) ? 1 : 0;
471         (my $base_format = $format) =~ s/-full$//o;
472         
473         if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
474                 print "Content-type: application/xml; charset=utf-8\n";
475                 if ($1) {
476                         my $list = $supercat
477                                 ->request("open-ils.supercat.$1.formats")
478                                 ->gather(1);
479
480                         print "\n";
481
482                         print "<formats>
483                                    <format>
484                                      <name>opac</name>
485                                      <type>text/html</type>
486                                    </format>";
487
488                         if ($1 eq 'record' or $1 eq 'isbn') {
489                                 print "<format>
490                                      <name>htmlholdings</name>
491                                      <type>text/html</type>
492                                    </format>
493                                    <format>
494                                      <name>html</name>
495                                      <type>text/html</type>
496                                    </format>
497                                    <format>
498                                      <name>htmlholdings-full</name>
499                                      <type>text/html</type>
500                                    </format>
501                                    <format>
502                                      <name>html-full</name>
503                                      <type>text/html</type>
504                                    </format>";
505                         }
506
507                         for my $h (@$list) {
508                                 my ($type) = keys %$h;
509                                 print "<format><name>$type</name><type>application/xml</type>";
510
511                                 for my $part ( qw/namespace_uri docs schema_location/ ) {
512                                         print "<$part>$$h{$type}{$part}</$part>"
513                                                 if ($$h{$type}{$part});
514                                 }
515                                 
516                                 print '</format>';
517
518                                 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
519                                         print "<format><name>$type-full</name><type>application/xml</type>";
520
521                                         for my $part ( qw/namespace_uri docs schema_location/ ) {
522                                                 print "<$part>$$h{$type}{$part}</$part>"
523                                                         if ($$h{$type}{$part});
524                                         }
525                                 
526                                         print '</format>';
527                                 }
528
529                         }
530
531                         print "</formats>\n";
532
533                         return Apache2::Const::OK;
534                 }
535
536                 my $list = $supercat
537                         ->request("open-ils.supercat.record.formats")
538                         ->gather(1);
539                                 
540                 push @$list,
541                         @{ $supercat
542                                 ->request("open-ils.supercat.metarecord.formats")
543                                 ->gather(1);
544                         };
545
546                 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
547                 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
548
549                 print "\n<formats>
550                            <format>
551                              <name>opac</name>
552                              <type>text/html</type>
553                            </format>
554                            <format>
555                              <name>htmlholdings</name>
556                              <type>text/html</type>
557                            </format>
558                            <format>
559                              <name>html</name>
560                              <type>text/html</type>
561                            </format>
562                            <format>
563                              <name>htmlholdings-full</name>
564                              <type>text/html</type>
565                            </format>
566                            <format>
567                              <name>html-full</name>
568                              <type>text/html</type>
569                            </format>";
570
571                 for my $h (@$list) {
572                         my ($type) = keys %$h;
573                         print "<format><name>$type</name><type>application/xml</type>";
574
575                         for my $part ( qw/namespace_uri docs schema_location/ ) {
576                                 print "<$part>$$h{$type}{$part}</$part>"
577                                         if ($$h{$type}{$part});
578                         }
579                         
580                         print '</format>';
581
582                         if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
583                                 print "<format><name>$type-full</name><type>application/xml</type>";
584
585                                 for my $part ( qw/namespace_uri docs schema_location/ ) {
586                                         print "<$part>$$h{$type}{$part}</$part>"
587                                                 if ($$h{$type}{$part});
588                                 }
589                                 
590                                 print '</format>';
591                         }
592
593                 }
594
595                 print "</formats>\n";
596
597
598                 return Apache2::Const::OK;
599         }
600
601         if ($format eq 'opac') {
602                 print "Location: $root/../../en-US/skin/default/xml/rresult.xml?m=$id\n\n"
603                         if ($type eq 'metarecord');
604                 print "Location: $root/../../en-US/skin/default/xml/rdetail.xml?r=$id\n\n"
605                         if ($type eq 'record');
606                 return 302;
607
608         } elsif ($base_format eq 'marc21') {
609
610                 my $ret = 200;    
611                 try {
612                         my $bib = $supercat->request( "open-ils.supercat.record.object.retrieve", $id )->gather(1)->[0];
613         
614                         my $r = MARC::Record->new_from_xml( $bib->marc, 'UTF-8', 'USMARC' );
615                         $r->delete_field( $_ ) for ($r->field(901));
616                 
617                         $r->append_fields(
618                                 MARC::Field->new(
619                                         901, '', '',
620                                         a => $bib->tcn_value,
621                                         b => $bib->tcn_source,
622                                         c => $bib->id
623                                 )
624                         );
625
626                         print "Content-type: application/octet-stream\n\n";
627                         print $r->as_usmarc;
628
629                 } otherwise {
630                         warn shift();
631                         
632                         print "Content-type: text/html; charset=utf-8\n\n";
633                         $apache->custom_response( 404, <<"                      HTML");
634                         <html>
635                                 <head>
636                                         <title>ERROR</title>
637                                 </head>
638                                 <body>
639                                         <br/>
640                                         <center>Couldn't fetch $id as MARC21.</center>
641                                 </body>
642                         </html>
643                         HTML
644                         $ret = 404;
645                 };
646
647                 return Apache2::Const::OK;
648
649         } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
650                 my $feed = create_record_feed(
651                         $type,
652                         $format => [ $id ],
653                         undef, undef,
654                         $flesh_feed
655                 );
656
657                 $feed->root($root);
658                 $feed->creator($host);
659                 $feed->update_ts(gmtime_ISO8601());
660                 $feed->link( unapi => $base) if ($flesh_feed);
661
662                 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
663                 print entityize($feed->toString) . "\n";
664
665                 return Apache2::Const::OK;
666         }
667
668         my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
669         $req->wait_complete;
670
671         if ($req->failed) {
672                 print "Content-type: text/html; charset=utf-8\n\n";
673                 $apache->custom_response( 404, <<"              HTML");
674                 <html>
675                         <head>
676                                 <title>$type $id not found!</title>
677                         </head>
678                         <body>
679                                 <br/>
680                                 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
681                         </body>
682                 </html>
683                 HTML
684                 return 404;
685         }
686
687         print "Content-type: application/xml; charset=utf-8\n\n";
688         print entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
689
690         return Apache2::Const::OK;
691 }
692
693
694 sub bookbag_feed {
695         my $apache = shift;
696         return Apache2::Const::DECLINED if (-e $apache->filename);
697
698         my $cgi = new CGI;
699
700         my $year = (gmtime())[5] + 1900;
701         my $host = $cgi->virtual_host || $cgi->server_name;
702
703         my $add_path = 0;
704         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
705                 my $rel_name = $cgi->url(-relative=>1);
706                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
707         }
708
709         my $url = $cgi->url(-path_info=>$add_path);
710         my $root = (split 'feed', $url)[0] . '/';
711         my $base = (split 'bookbag', $url)[0] . '/bookbag';
712         my $unapi = (split 'feed', $url)[0] . '/unapi';
713
714         $root =~ s{(?<!http:)//}{/}go;
715         $base =~ s{(?<!http:)//}{/}go;
716         $unapi =~ s{(?<!http:)//}{/}go;
717
718         my $path = $cgi->path_info;
719         #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
720
721         my ($id,$type) = reverse split '/', $path;
722         my $flesh_feed = ($type =~ /-full$/o) ? 1 : 0;
723
724         my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
725         return Apache2::Const::NOT_FOUND unless($bucket);
726
727         my $bucket_tag = "tag:$host,$year:record_bucket/$id";
728         if ($type eq 'opac') {
729                 print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
730                         join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
731                         "\n\n";
732                 return 302;
733         }
734
735         my $feed = create_record_feed(
736                 'record',
737                 $type,
738                 [ map { $_->target_biblio_record_entry } @{ $bucket->items } ],
739                 $unapi,
740                 undef,
741                 $flesh_feed
742         );
743         $feed->root($root);
744         $feed->id($bucket_tag);
745
746         $feed->title("Items in Book Bag [".$bucket->name."]");
747         $feed->creator($host);
748         $feed->update_ts(gmtime_ISO8601());
749
750         $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
751         $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
752         $feed->link(html => $base . "/html-full/$id" => 'text/html');
753         $feed->link(unapi => $unapi);
754
755         $feed->link(
756                 OPAC =>
757                 '/opac/en-US/skin/default/xml/rresult.xml?rt=list&' .
758                         join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
759                 'text/html'
760         );
761
762
763         print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
764         print entityize($feed->toString) . "\n";
765
766         return Apache2::Const::OK;
767 }
768
769 sub changes_feed {
770         my $apache = shift;
771         return Apache2::Const::DECLINED if (-e $apache->filename);
772
773         my $cgi = new CGI;
774
775         my $year = (gmtime())[5] + 1900;
776         my $host = $cgi->virtual_host || $cgi->server_name;
777
778         my $add_path = 0;
779         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
780                 my $rel_name = $cgi->url(-relative=>1);
781                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
782         }
783
784         my $url = $cgi->url(-path_info=>$add_path);
785         my $root = (split 'feed', $url)[0];
786         my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
787         my $unapi = (split 'feed', $url)[0] . 'unapi';
788
789         my $path = $cgi->path_info;
790         #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
791
792         $path =~ s/^\/(?:feed\/)?freshmeat\///og;
793         
794         my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
795         my $flesh_feed = ($type =~ /-full$/o) ? 1 : 0;
796         $limit ||= 10;
797
798         my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
799
800         #if ($type eq 'opac') {
801         #       print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
802         #               join('&', map { "rl=" . $_ } @$list) .
803         #               "\n\n";
804         #       return 302;
805         #}
806
807         my $feed = create_record_feed( 'record', $type, $list, $unapi, undef, $flesh_feed);
808         $feed->root($root);
809
810         if ($date) {
811                 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
812         } else {
813                 $feed->title("$limit most recent $rtype ${axis}s");
814         }
815
816         $feed->creator($host);
817         $feed->update_ts(gmtime_ISO8601());
818
819         $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
820         $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
821         $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
822         $feed->link(unapi => $unapi);
823
824         $feed->link(
825                 OPAC =>
826                 '/opac/en-US/skin/default/xml/rresult.xml?rt=list&' .
827                         join('&', map { 'rl=' . $_} @$list ),
828                 'text/html'
829         );
830
831
832         print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
833         print entityize($feed->toString) . "\n";
834
835         return Apache2::Const::OK;
836 }
837
838 sub opensearch_osd {
839         my $version = shift;
840         my $lib = shift;
841         my $class = shift;
842         my $base = shift;
843
844         if ($version eq '1.0') {
845                 print <<OSD;
846 Content-type: application/opensearchdescription+xml; charset=utf-8
847
848 <?xml version="1.0" encoding="UTF-8"?>
849 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
850   <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&amp;startPage={startPage}&amp;startIndex={startIndex}&amp;count={count}</Url>
851   <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
852   <ShortName>$lib</ShortName>
853   <LongName>Search $lib</LongName>
854   <Description>Search the $lib OPAC by $class.</Description>
855   <Tags>$lib book library</Tags>
856   <SampleSearch>harry+potter</SampleSearch>
857   <Developer>Mike Rylander for GPLS/PINES</Developer>
858   <Contact>feedback\@open-ils.org</Contact>
859   <SyndicationRight>open</SyndicationRight>
860   <AdultContent>false</AdultContent>
861 </OpenSearchDescription>
862 OSD
863         } else {
864                 print <<OSD;
865 Content-type: application/opensearchdescription+xml; charset=utf-8
866
867 <?xml version="1.0" encoding="UTF-8"?>
868 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
869   <ShortName>$lib</ShortName>
870   <Description>Search the $lib OPAC by $class.</Description>
871   <Tags>$lib book library</Tags>
872   <Url type="application/rss+xml"
873        template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
874   <Url type="application/atom+xml"
875        template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
876   <Url type="application/x-mods3+xml"
877        template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
878   <Url type="application/x-mods+xml"
879        template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
880   <Url type="application/x-marcxml+xml"
881        template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
882   <Url type="text/html"
883        template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
884   <LongName>Search $lib</LongName>
885   <Query role="example" searchTerms="harry+potter" />
886   <Developer>Mike Rylander for GPLS/PINES</Developer>
887   <Contact>feedback\@open-ils.org</Contact>
888   <SyndicationRight>open</SyndicationRight>
889   <AdultContent>false</AdultContent>
890   <Language>en-US</Language>
891   <OutputEncoding>UTF-8</OutputEncoding>
892   <InputEncoding>UTF-8</InputEncoding>
893 </OpenSearchDescription>
894 OSD
895         }
896
897         return Apache2::Const::OK;
898 }
899
900 sub opensearch_feed {
901         my $apache = shift;
902         return Apache2::Const::DECLINED if (-e $apache->filename);
903
904         my $cgi = new CGI;
905         my $year = (gmtime())[5] + 1900;
906
907         my $host = $cgi->virtual_host || $cgi->server_name;
908
909         my $add_path = 0;
910         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
911                 my $rel_name = $cgi->url(-relative=>1);
912                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
913         }
914
915         my $url = $cgi->url(-path_info=>$add_path);
916         my $root = (split 'opensearch', $url)[0];
917         my $base = (split 'opensearch', $url)[0] . 'opensearch';
918         my $unapi = (split 'opensearch', $url)[0] . 'unapi';
919
920         my $path = $cgi->path_info;
921         #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
922
923         if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
924                 
925                 my $version = $1;
926                 my $lib = uc($2);
927                 my $class = $3;
928
929                 if (!$lib || $lib eq '-') {
930                         $lib = $actor->request(
931                                 'open-ils.actor.org_unit_list.search' => parent_ou => undef
932                         )->gather(1)->[0]->shortname;
933                 }
934
935                 if ($class eq '-') {
936                         $class = 'keyword';
937                 }
938
939                 return opensearch_osd($version, $lib, $class, $base);
940         }
941
942
943         my $page = $cgi->param('startPage') || 1;
944         my $offset = $cgi->param('startIndex') || 1;
945         my $limit = $cgi->param('count') || 10;
946
947         $page = 1 if ($page !~ /^\d+$/);
948         $offset = 1 if ($offset !~ /^\d+$/);
949         $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
950
951         if ($page > 1) {
952                 $offset = ($page - 1) * $limit;
953         } else {
954                 $offset -= 1;
955         }
956
957         my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
958         (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
959
960         $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
961         $lang = '' if ($lang eq '*');
962
963         $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
964         $sort ||= '';
965         $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
966         $sortdir ||= '';
967
968         $terms .= " " if ($terms && $cgi->param('searchTerms'));
969         $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
970
971         $class = $cgi->param('searchClass') if $cgi->param('searchClass');
972         $class ||= '-';
973
974         $type = $cgi->param('responseType') if $cgi->param('responseType');
975         $type ||= '-';
976
977         $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
978         $org ||= '-';
979
980
981         my $kwt = $cgi->param('kw');
982         my $tit = $cgi->param('ti');
983         my $aut = $cgi->param('au');
984         my $sut = $cgi->param('su');
985         my $set = $cgi->param('se');
986
987         $terms .= " " if ($terms && $kwt);
988         $terms .= "keyword: $kwt" if ($kwt);
989         $terms .= " " if ($terms && $tit);
990         $terms .= "title: $tit" if ($tit);
991         $terms .= " " if ($terms && $aut);
992         $terms .= "author: $aut" if ($aut);
993         $terms .= " " if ($terms && $sut);
994         $terms .= "subject: $sut" if ($sut);
995         $terms .= " " if ($terms && $set);
996         $terms .= "series: $set" if ($set);
997
998         if ($version eq '1.0') {
999                 $type = 'rss2';
1000         } elsif ($type eq '-') {
1001                 $type = 'atom';
1002         }
1003         my $flesh_feed = ($type =~ /-full$/o) ? 1 : 0;
1004
1005         if ($terms eq 'help') {
1006                 print $cgi->header(-type => 'text/html');
1007                 print <<"               HTML";
1008                         <html>
1009                          <head>
1010                           <title>just type something!</title>
1011                          </head>
1012                          <body>
1013                           <p>You are in a maze of dark, twisty stacks, all alike.</p>
1014                          </body>
1015                         </html>
1016                 HTML
1017                 return Apache2::Const::OK;
1018     }
1019         
1020         $terms = decode_utf8($terms);
1021         $lang = 'eng' if ($lang eq 'en-US');
1022
1023         $log->debug("OpenSearch terms: $terms");
1024
1025         my $org_unit;
1026         if ($org eq '-') {
1027                 $org_unit = $actor->request(
1028                         'open-ils.actor.org_unit_list.search' => parent_ou => undef
1029                 )->gather(1);
1030         } elsif ($org !~ /^\d+$/o) {
1031                 $org_unit = $actor->request(
1032                         'open-ils.actor.org_unit_list.search' => shortname => uc($org)
1033                 )->gather(1);
1034         } else {
1035                 $org_unit = $actor->request(
1036                         'open-ils.actor.org_unit_list.search' => id => $org
1037                 )->gather(1);
1038         }
1039
1040     my $recs = $search->request(
1041         'open-ils.search.biblio.multiclass.query' => {
1042                         org_unit        => $org_unit->[0]->id,
1043                         offset          => $offset,
1044                         limit           => $limit,
1045                         sort            => $sort,
1046                         sort_dir        => $sortdir,
1047                         ($lang ?    ( 'language' => $lang    ) : ()),
1048                 } => $terms => 1
1049         )->gather(1);
1050
1051         $log->debug("Hits for [$terms]: $recs->{count}");
1052
1053         my $feed = create_record_feed(
1054                 'record',
1055                 $type,
1056                 [ map { $_->[0] } @{$recs->{ids}} ],
1057                 $unapi,
1058                 $org,
1059                 $flesh_feed
1060         );
1061
1062         $log->debug("Feed created...");
1063
1064         $feed->root($root);
1065         $feed->lib($org);
1066         $feed->search($terms);
1067         $feed->class($class);
1068
1069         $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1070
1071         $feed->creator($host);
1072         $feed->update_ts(gmtime_ISO8601());
1073
1074         $feed->_create_node(
1075                 $feed->{item_xpath},
1076                 'http://a9.com/-/spec/opensearch/1.1/',
1077                 'totalResults',
1078                 $recs->{count},
1079         );
1080
1081         $feed->_create_node(
1082                 $feed->{item_xpath},
1083                 'http://a9.com/-/spec/opensearch/1.1/',
1084                 'startIndex',
1085                 $offset + 1,
1086         );
1087
1088         $feed->_create_node(
1089                 $feed->{item_xpath},
1090                 'http://a9.com/-/spec/opensearch/1.1/',
1091                 'itemsPerPage',
1092                 $limit,
1093         );
1094
1095         $log->debug("...basic feed data added...");
1096
1097         $feed->link(
1098                 next =>
1099                 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1100                 'application/opensearch+xml'
1101         ) if ($offset + $limit < $recs->{count});
1102
1103         $feed->link(
1104                 previous =>
1105                 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1106                 'application/opensearch+xml'
1107         ) if ($offset);
1108
1109         $feed->link(
1110                 self =>
1111                 $base .  "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1112                 'application/opensearch+xml'
1113         );
1114
1115         $feed->link(
1116                 alternate =>
1117                 $base .  "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1118                 'application/rss+xml'
1119         );
1120
1121         $feed->link(
1122                 atom =>
1123                 $base .  "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1124                 'application/atom+xml'
1125         );
1126
1127         $feed->link(
1128                 'html' =>
1129                 $base .  "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1130                 'text/html'
1131         );
1132
1133         $feed->link(
1134                 'html-full' =>
1135                 $base .  "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1136                 'text/html'
1137         );
1138
1139         $feed->link( 'unapi-server' => $unapi);
1140
1141         $log->debug("...feed links added...");
1142
1143 #       $feed->link(
1144 #               opac =>
1145 #               $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1146 #                       join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1147 #               'text/html'
1148 #       );
1149
1150         print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1151
1152         $log->debug("...and feed returned.");
1153
1154         return Apache2::Const::OK;
1155 }
1156
1157 sub create_record_feed {
1158         my $search = shift;
1159         my $type = shift;
1160         my $records = shift;
1161         my $unapi = shift;
1162
1163         my $lib = uc(shift()) || '-';
1164         my $flesh = shift;
1165         $flesh = 1 if (!defined($flesh));
1166
1167         my $cgi = new CGI;
1168         my $base = $cgi->url;
1169         my $host = $cgi->virtual_host || $cgi->server_name;
1170
1171         my $year = (gmtime())[5] + 1900;
1172
1173         my $flesh_feed = ($type =~ s/-full$//o) ? 1 : 0;
1174
1175         my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1176         $feed->base($base) if ($flesh);
1177         $feed->unapi($unapi) if ($flesh);
1178
1179         $type = 'atom' if ($type eq 'html');
1180         $type = 'marcxml' if ($type eq 'htmlholdings');
1181
1182         #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1183
1184         my $count = 0;
1185         for my $record (@$records) {
1186                 next unless($record);
1187
1188                 #my $rec = $record->id;
1189                 my $rec = $record;
1190
1191                 my $item_tag = "tag:$host,$year:biblio-record_entry/$rec/$lib";
1192                 $item_tag = "tag:$host,$year:isbn/$rec/$lib" if ($search eq 'isbn');
1193
1194                 my $xml = $supercat->request(
1195                         "open-ils.supercat.$search.$type.retrieve",
1196                         $rec
1197                 )->gather(1);
1198                 next unless $xml;
1199
1200                 my $node = $feed->add_item($xml);
1201                 next unless $node;
1202
1203                 $xml = '';
1204                 if ($lib && $type eq 'marcxml' &&  $flesh) {
1205                         my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib );
1206                         while ( !$r->complete ) {
1207                                 $xml .= join('', map {$_->content} $r->recv);
1208                         }
1209                         $xml .= join('', map {$_->content} $r->recv);
1210                         $node->add_holdings($xml);
1211                 }
1212
1213                 $node->id($item_tag);
1214                 #$node->update_ts(clense_ISO8601($record->edit_date));
1215                 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh);
1216                 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh);
1217                 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1218                 $node->link('unapi-id' => $item_tag) if ($flesh);
1219         }
1220
1221         return $feed;
1222 }
1223
1224 sub entityize {
1225         my $stuff = NFC(shift());
1226         $stuff =~ s/&(?!\S+;)/&amp;/gso;
1227         $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
1228         return $stuff;
1229 }
1230
1231 sub string_browse {
1232         my $apache = shift;
1233         return Apache2::Const::DECLINED if (-e $apache->filename);
1234
1235         my $cgi = new CGI;
1236         my $year = (gmtime())[5] + 1900;
1237
1238         my $host = $cgi->virtual_host || $cgi->server_name;
1239
1240         my $add_path = 0;
1241         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1242                 my $rel_name = $cgi->url(-relative=>1);
1243                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1244         }
1245
1246         my $url = $cgi->url(-path_info=>$add_path);
1247         my $root = (split 'browse', $url)[0];
1248         my $base = (split 'browse', $url)[0] . 'browse';
1249         my $unapi = (split 'browse', $url)[0] . 'unapi';
1250
1251         my $path = $cgi->path_info;
1252         $path =~ s/^\///og;
1253
1254         my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1255         #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1256
1257         $site ||= $cgi->param('searchOrg');
1258         $page ||= $cgi->param('startPage') || 0;
1259         $page_size ||= $cgi->param('count') || 9;
1260
1261         $page = 0 if ($page !~ /^-?\d+$/);
1262
1263         my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1264         my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1265
1266         unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1267                 warn "something's wrong...";
1268                 warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1269                 return undef;
1270         }
1271
1272         $string = decode_utf8($string);
1273         $string =~ s/\+/ /go;
1274         $string =~ s/'//go;
1275
1276         my $tree = $supercat->request(
1277                 "open-ils.supercat.$axis.browse",
1278                 $string,
1279                 $site,
1280                 $page_size,
1281                 $page
1282         )->gather(1);
1283
1284         my ($header,$content) = $browse_types{$axis}{$format}->($tree,$prev,$next);
1285         print $header.$content;
1286         return Apache2::Const::OK;
1287 }
1288
1289 our %qualifier_map = (
1290
1291     # Som EG qualifiers
1292     'eg.site'               => 'site',
1293     'eg.sort'               => 'sort',
1294     'eg.direction'          => 'dir',
1295     'eg.available'          => 'available',
1296
1297     # Title class:
1298     'eg.title'              => 'title',
1299     'dc.title'              => 'title',
1300     'bib.titleabbreviated'  => 'title|abbreviated',
1301     'bib.titleuniform'      => 'title|uniform',
1302     'bib.titletranslated'   => 'title|translated',
1303     'bib.titlealternative'  => 'title',
1304     'bib.titleseries'       => 'series',
1305     'eg.series'             => 'title',
1306
1307     # Author/Name class:
1308     'eg.author'             => 'title',
1309     'eg.name'               => 'title',
1310     'creator'               => 'author',
1311     'dc.creator'            => 'author',
1312     'dc.contributer'        => 'author',
1313     'dc.publisher'          => 'keyword',
1314     'bib.name'              => 'author',
1315     'bib.namepersonal'      => 'author|personal',
1316     'bib.namepersonalfamily'=> 'author|personal',
1317     'bib.namepersonalgiven' => 'author|personal',
1318     'bib.namecorporate'     => 'author|corporate',
1319     'bib.nameconference'    => 'author|conference',
1320
1321     # Subject class:
1322     'eg.subject'            => 'subject',
1323     'dc.subject'            => 'subject',
1324     'bib.subjectplace'      => 'subject|geographic',
1325     'bib.subjecttitle'      => 'keyword',
1326     'bib.subjectname'       => 'subject|name',
1327     'bib.subjectoccupation' => 'keyword',
1328
1329     # Keyword class:
1330     'eg.keyword'            => 'keyword',
1331     'srw.serverchoice'      => 'keyword',
1332
1333     # Identifiers:
1334     'dc.identifier'         => 'keyword',
1335
1336     # Dates:
1337     'bib.dateissued'        => undef,
1338     'bib.datecreated'       => undef,
1339     'bib.datevalid'         => undef,
1340     'bib.datemodified'      => undef,
1341     'bib.datecopyright'     => undef,
1342
1343     # Resource Type:
1344     'dc.type'               => undef,
1345
1346     # Format:
1347     'dc.format'             => undef,
1348
1349     # Genre:
1350     'bib.genre'             => 'keyword',
1351
1352     # Target Audience:
1353     'bib.audience'          => undef,
1354
1355     # Place of Origin:
1356     'bib.originplace'       => undef,
1357
1358     # Language
1359     'dc.language'           => 'lang',
1360
1361     # Edition
1362     'bib.edition'           => 'keyword',
1363
1364     # Part:
1365     'bib.volume'            => 'keyword',
1366     'bib.issue'             => 'keyword',
1367     'bib.startpage'         => 'keyword',
1368     'bib.endpage'           => 'keyword',
1369
1370     # Issuance:
1371     'bib.issuance'          => 'keyword',
1372 );
1373
1374 our %qualifier_ids = (
1375                 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1376                 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1377                 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1378                 srw     => ''
1379 );
1380
1381 our %nested_qualifier_map = (
1382                 eg => {
1383                         site            => ['site','Evergreen Site Code (shortname)'],
1384                         sort            => ['sort','Sort on relevance, title, author, pubdate, create_date or edit_date'],
1385                         direction       => ['dir','Sort direction (asc|desc)'],
1386                         available       => ['available','Filter to availble (true|false)'],
1387                         title           => ['title'],
1388                         author          => ['author'],
1389                         name            => ['author'],
1390                         subject         => ['subject'],
1391                         keyword         => ['keyword'],
1392                         series          => ['series'],
1393                 },
1394                 dc => {
1395                         title           => ['title'],
1396                         creator         => ['author'],
1397                         contributor     => ['author'],
1398                         publisher       => ['keyword'],
1399                         subject         => ['subject'],
1400                         identifier      => ['keyword'],
1401                         type            => [undef],
1402                         format          => [undef],
1403                         language        => ['lang'],
1404                 },
1405                 bib => {
1406                 # Title class:
1407                 titleAbbreviated        => ['title'],
1408                     titleUniform                => ['title'],
1409                         titleTranslated         => ['title'],
1410                 titleAlternative        => ['title'],
1411                     titleSeries                 => ['series'],
1412
1413     # Author/Name class:
1414                         name                            => ['author'],
1415                         namePersonal            => ['author'],
1416                         namePersonalFamily      => ['author'],
1417                         namePersonalGiven       => ['author'],
1418                         nameCorporate           => ['author'],
1419                         nameConference          => ['author'],
1420
1421                 # Subject class:
1422                         subjectPlace            => ['subject'],
1423                         subjectTitle            => ['keyword'],
1424                         subjectName                     => ['subject|name'],
1425                         subjectOccupation       => ['keyword'],
1426
1427     # Keyword class:
1428
1429     # Dates:
1430                         dateIssued                      => [undef],
1431                         dateCreated                     => [undef],
1432                         dateValid                       => [undef],
1433                         dateModified            => [undef],
1434                         dateCopyright           => [undef],
1435
1436     # Genre:
1437                         genre                           => ['keyword'],
1438
1439     # Target Audience:
1440                         audience                        => [undef],
1441
1442     # Place of Origin:
1443                         originPlace                     => [undef],
1444
1445     # Edition
1446                         edition                         => ['keyword'],
1447
1448     # Part:
1449                         volume                          => ['keyword'],
1450                         issue                           => ['keyword'],
1451                         startPage                       => ['keyword'],
1452                         endPage                         => ['keyword'],
1453
1454     # Issuance:
1455                         issuance                        => ['keyword'],
1456                 },
1457                 srw     => {
1458                         serverChoice            => ['keyword'],
1459                 },
1460 );
1461
1462
1463 my $base_explain = <<XML;
1464 <explain
1465                 id="evergreen-sru-explain-full"
1466                 authoritative="true"
1467                 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1468                 xmlns="http://explain.z3950.org/dtd/2.0/">
1469         <serverInfo transport="http" protocol="SRU" version="1.1">
1470                 <host/>
1471                 <port/>
1472                 <database/>
1473         </serverInfo>
1474
1475         <databaseInfo>
1476                 <title primary="true"/>
1477                 <description primary="true"/>
1478         </databaseInfo>
1479
1480         <indexInfo>
1481                 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1482         </indexInfo>
1483
1484         <schemaInfo>
1485                 <schema
1486                                 identifier="info:srw/schema/1/marcxml-v1.1"
1487                                 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1488                                 sort="true"
1489                                 retrieve="true"
1490                                 name="marcxml">
1491                         <title>MARC21Slim (marcxml)</title>
1492                 </schema>
1493         </schemaInfo>
1494
1495         <configInfo>
1496                 <default type="numberOfRecords">10</default>
1497                 <default type="contextSet">eg</default>
1498                 <default type="index">keyword</default>
1499                 <default type="relation">all</default>
1500                 <default type="sortSchema">marcxml</default>
1501                 <default type="retrieveSchema">marcxml</default>
1502                 <setting type="maximumRecords">10</setting>
1503                 <supports type="relationModifier">relevant</supports>
1504                 <supports type="relationModifier">stem</supports>
1505                 <supports type="relationModifier">fuzzy</supports>
1506                 <supports type="relationModifier">word</supports>
1507         </configInfo>
1508
1509 </explain>
1510 XML
1511
1512
1513 my $ex_doc;
1514 sub sru_search {
1515     my $cgi = new CGI;
1516
1517     my $req = SRU::Request->newFromCGI( $cgi );
1518     my $resp = SRU::Response->newFromRequest( $req );
1519
1520     if ( $resp->type eq 'searchRetrieve' ) {
1521                 my $cql_query = $req->query;
1522                 my $search_string = $req->cql->toEvergreen;
1523
1524         my $offset = $req->startRecord;
1525         $offset-- if ($offset);
1526         $offset ||= 0;
1527
1528         my $limit = $req->maximumRecords;
1529         $limit ||= 10;
1530
1531         warn "SRU search string [$cql_query] converted to [$search_string]\n";
1532
1533                 my $recs = $search->request(
1534                         'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1535                 )->gather(1);
1536
1537         my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1538
1539         $resp->addRecord(
1540             SRU::Response::Record->new(
1541                 recordSchema    => 'info:srw/schema/1/marcxml-v1.1',
1542                 recordData => $_->marc
1543             )
1544         ) for @$bre;
1545
1546         $resp->numberOfRecords($recs->{count});
1547
1548     } elsif ( $resp->type eq 'explain' ) {
1549                 if (!$ex_doc) {
1550                         my $host = $cgi->virtual_host || $cgi->server_name;
1551
1552                         my $add_path = 0;
1553                         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1554                                 my $rel_name = $cgi->url(-relative=>1);
1555                                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1556                         }
1557                         my $base = $cgi->url(-base=>1);
1558                         my $url = $cgi->url(-path_info=>$add_path);
1559                         $url =~ s/^$base\///o;
1560
1561                         my $doc = $parser->parse_string($base_explain);
1562                         my $e = $doc->documentElement;
1563                         $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
1564                         $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
1565                         $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
1566
1567                         for my $name ( keys %OpenILS::WWW::SuperCat::nested_qualifier_map ) {
1568
1569                                 my $identifier = $OpenILS::WWW::SuperCat::qualifier_ids{ $name };
1570
1571                                 next unless $identifier;
1572
1573                                 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
1574                                 $set_node->setAttribute( identifier => $identifier );
1575                                 $set_node->setAttribute( name => $name );
1576
1577                                 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
1578
1579                                 for my $index ( keys %{ $OpenILS::WWW::SuperCat::nested_qualifier_map{$name} } ) {
1580                                         my $desc = $OpenILS::WWW::SuperCat::nested_qualifier_map{$name}{$index}[1] || $index;
1581
1582                                         my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
1583
1584                                         my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
1585                                         $map_node->appendChild( $name_node );
1586
1587                                         my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
1588
1589                                         my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
1590                                         $index_node->appendChild( $title_node );
1591                                         $index_node->appendChild( $map_node );
1592
1593                                         $index_node->setAttribute( id => $name . '.' . $index );
1594                                         $title_node->appendText( $desc );
1595                                         $name_node->setAttribute( set => $name );
1596                                         $name_node->appendText($index );
1597
1598                                         $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
1599                                 }
1600                         }
1601
1602                         $ex_doc = $e->toString;
1603                 }
1604
1605                 $resp->record(
1606                         SRU::Response::Record->new(
1607                                 recordSchema    => 'info:srw/cql-context-set/2/zeerex-1.1',
1608                                 recordData              => $ex_doc
1609                         )
1610                 );
1611         }
1612
1613         print $cgi->header( -type => 'application/xml' );
1614         print entityize($resp->asXML) . "\n";
1615     return Apache2::Const::OK;
1616 }
1617
1618
1619 {
1620     package CQL::BooleanNode;
1621
1622     sub toEvergreen {
1623         my $self     = shift;
1624         my $left     = $self->left();
1625         my $right    = $self->right();
1626         my $leftStr  = $left->toEvergreen;
1627         my $rightStr = $right->toEvergreen();
1628
1629         my $op =  '||' if uc $self->op() eq 'OR';
1630         $op ||=  '&&';
1631
1632         return  "$leftStr $rightStr";
1633     }
1634
1635     package CQL::TermNode;
1636
1637     sub toEvergreen {
1638         my $self      = shift;
1639         my $qualifier = $self->getQualifier();
1640         my $term      = $self->getTerm();
1641         my $relation  = $self->getRelation();
1642
1643         my $query;
1644         if ( $qualifier ) {
1645                         my ($qset, $qname) = split(/\./, $qualifier);
1646
1647                         warn "!!! $qset, $qname   $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0]\n";
1648
1649             if ( exists($OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}) ) {
1650                 $qualifier = $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0] || 'kw';
1651                         }
1652
1653             my @modifiers = $relation->getModifiers();
1654
1655             my $base = $relation->getBase();
1656             if ( grep { $base eq $_ } qw/= scr exact all/ ) {
1657
1658                 my $quote_it = 1;
1659                 foreach my $m ( @modifiers ) {
1660                     if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
1661                         $quote_it = 0;
1662                         last;
1663                     }
1664                 }
1665
1666                 $quote_it = 0 if ( $base eq 'all' );
1667                 $term = maybeQuote($term) if $quote_it;
1668
1669             } else {
1670                 croak( "Evergreen doesn't support the $base relations" );
1671             }
1672
1673
1674         } else {
1675             $qualifier = "kw";
1676         }
1677
1678         return "$qualifier:$term";
1679     }
1680 }
1681
1682 1;