Address a few RSS and Atom feed validation problems:
[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();
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
660                 $feed->update_ts();
661
662                 $feed->link( unapi => $base) if ($flesh_feed);
663
664                 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
665                 print entityize($feed->toString) . "\n";
666
667                 return Apache2::Const::OK;
668         }
669
670         my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
671         $req->wait_complete;
672
673         if ($req->failed) {
674                 print "Content-type: text/html; charset=utf-8\n\n";
675                 $apache->custom_response( 404, <<"              HTML");
676                 <html>
677                         <head>
678                                 <title>$type $id not found!</title>
679                         </head>
680                         <body>
681                                 <br/>
682                                 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
683                         </body>
684                 </html>
685                 HTML
686                 return 404;
687         }
688
689         print "Content-type: application/xml; charset=utf-8\n\n";
690         print entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
691
692         return Apache2::Const::OK;
693 }
694
695
696 sub bookbag_feed {
697         my $apache = shift;
698         return Apache2::Const::DECLINED if (-e $apache->filename);
699
700         my $cgi = new CGI;
701
702         my $year = (gmtime())[5] + 1900;
703         my $host = $cgi->virtual_host || $cgi->server_name;
704
705         my $add_path = 0;
706         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
707                 my $rel_name = $cgi->url(-relative=>1);
708                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
709         }
710
711         my $url = $cgi->url(-path_info=>$add_path);
712         my $root = (split 'feed', $url)[0] . '/';
713         my $base = (split 'bookbag', $url)[0] . '/bookbag';
714         my $unapi = (split 'feed', $url)[0] . '/unapi';
715
716         $root =~ s{(?<!http:)//}{/}go;
717         $base =~ s{(?<!http:)//}{/}go;
718         $unapi =~ s{(?<!http:)//}{/}go;
719
720         my $path = $cgi->path_info;
721         #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
722
723         my ($id,$type) = reverse split '/', $path;
724         my $flesh_feed = ($type =~ /-full$/o) ? 1 : 0;
725
726         my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
727         return Apache2::Const::NOT_FOUND unless($bucket);
728
729         my $bucket_tag = "tag:$host,$year:record_bucket/$id";
730         if ($type eq 'opac') {
731                 print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
732                         join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
733                         "\n\n";
734                 return 302;
735         }
736
737         my $feed = create_record_feed(
738                 'record',
739                 $type,
740                 [ map { $_->target_biblio_record_entry } @{ $bucket->items } ],
741                 $unapi,
742                 undef,
743                 $flesh_feed
744         );
745         $feed->root($root);
746         $feed->id($bucket_tag);
747
748         $feed->title("Items in Book Bag [".$bucket->name."]");
749         $feed->creator($host);
750         $feed->update_ts();
751
752         $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
753         $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
754         $feed->link(html => $base . "/html-full/$id" => 'text/html');
755         $feed->link(unapi => $unapi);
756
757         $feed->link(
758                 OPAC =>
759                 $host . '/opac/en-US/skin/default/xml/rresult.xml?rt=list&' .
760                         join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
761                 'text/html'
762         );
763
764
765         print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
766         print entityize($feed->toString) . "\n";
767
768         return Apache2::Const::OK;
769 }
770
771 sub changes_feed {
772         my $apache = shift;
773         return Apache2::Const::DECLINED if (-e $apache->filename);
774
775         my $cgi = new CGI;
776
777         my $year = (gmtime())[5] + 1900;
778         my $host = $cgi->virtual_host || $cgi->server_name;
779
780         my $add_path = 0;
781         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
782                 my $rel_name = $cgi->url(-relative=>1);
783                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
784         }
785
786         my $url = $cgi->url(-path_info=>$add_path);
787         my $root = (split 'feed', $url)[0];
788         my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
789         my $unapi = (split 'feed', $url)[0] . 'unapi';
790
791         my $path = $cgi->path_info;
792         #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
793
794         $path =~ s/^\/(?:feed\/)?freshmeat\///og;
795         
796         my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
797         my $flesh_feed = ($type =~ /-full$/o) ? 1 : 0;
798         $limit ||= 10;
799
800         my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
801
802         #if ($type eq 'opac') {
803         #       print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
804         #               join('&', map { "rl=" . $_ } @$list) .
805         #               "\n\n";
806         #       return 302;
807         #}
808
809         my $feed = create_record_feed( 'record', $type, $list, $unapi, undef, $flesh_feed);
810         $feed->root($root);
811
812         if ($date) {
813                 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
814         } else {
815                 $feed->title("$limit most recent $rtype ${axis}s");
816         }
817
818         $feed->creator($host);
819         $feed->update_ts();
820
821         $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
822         $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
823         $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
824         $feed->link(unapi => $unapi);
825
826         $feed->link(
827                 OPAC =>
828                 $host . '/opac/en-US/skin/default/xml/rresult.xml?rt=list&' .
829                         join('&', map { 'rl=' . $_} @$list ),
830                 'text/html'
831         );
832
833
834         print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
835         print entityize($feed->toString) . "\n";
836
837         return Apache2::Const::OK;
838 }
839
840 sub opensearch_osd {
841         my $version = shift;
842         my $lib = shift;
843         my $class = shift;
844         my $base = shift;
845
846         if ($version eq '1.0') {
847                 print <<OSD;
848 Content-type: application/opensearchdescription+xml; charset=utf-8
849
850 <?xml version="1.0" encoding="UTF-8"?>
851 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
852   <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&amp;startPage={startPage}&amp;startIndex={startIndex}&amp;count={count}</Url>
853   <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
854   <ShortName>$lib</ShortName>
855   <LongName>Search $lib</LongName>
856   <Description>Search the $lib OPAC by $class.</Description>
857   <Tags>$lib book library</Tags>
858   <SampleSearch>harry+potter</SampleSearch>
859   <Developer>Mike Rylander for GPLS/PINES</Developer>
860   <Contact>feedback\@open-ils.org</Contact>
861   <SyndicationRight>open</SyndicationRight>
862   <AdultContent>false</AdultContent>
863 </OpenSearchDescription>
864 OSD
865         } else {
866                 print <<OSD;
867 Content-type: application/opensearchdescription+xml; charset=utf-8
868
869 <?xml version="1.0" encoding="UTF-8"?>
870 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
871   <ShortName>$lib</ShortName>
872   <Description>Search the $lib OPAC by $class.</Description>
873   <Tags>$lib book library</Tags>
874   <Url type="application/rss+xml"
875        template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
876   <Url type="application/atom+xml"
877        template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
878   <Url type="application/x-mods3+xml"
879        template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
880   <Url type="application/x-mods+xml"
881        template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
882   <Url type="application/x-marcxml+xml"
883        template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
884   <Url type="text/html"
885        template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&amp;startPage={startPage?}&amp;startIndex={startIndex?}&amp;count={count?}&amp;searchLang={language?}"/>
886   <LongName>Search $lib</LongName>
887   <Query role="example" searchTerms="harry+potter" />
888   <Developer>Mike Rylander for GPLS/PINES</Developer>
889   <Contact>feedback\@open-ils.org</Contact>
890   <SyndicationRight>open</SyndicationRight>
891   <AdultContent>false</AdultContent>
892   <Language>en-US</Language>
893   <OutputEncoding>UTF-8</OutputEncoding>
894   <InputEncoding>UTF-8</InputEncoding>
895 </OpenSearchDescription>
896 OSD
897         }
898
899         return Apache2::Const::OK;
900 }
901
902 sub opensearch_feed {
903         my $apache = shift;
904         return Apache2::Const::DECLINED if (-e $apache->filename);
905
906         my $cgi = new CGI;
907         my $year = (gmtime())[5] + 1900;
908
909         my $host = $cgi->virtual_host || $cgi->server_name;
910
911         my $add_path = 0;
912         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
913                 my $rel_name = $cgi->url(-relative=>1);
914                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
915         }
916
917         my $url = $cgi->url(-path_info=>$add_path);
918         my $root = (split 'opensearch', $url)[0];
919         my $base = (split 'opensearch', $url)[0] . 'opensearch';
920         my $unapi = (split 'opensearch', $url)[0] . 'unapi';
921
922         my $path = $cgi->path_info;
923         #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
924
925         if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
926                 
927                 my $version = $1;
928                 my $lib = uc($2);
929                 my $class = $3;
930
931                 if (!$lib || $lib eq '-') {
932                         $lib = $actor->request(
933                                 'open-ils.actor.org_unit_list.search' => parent_ou => undef
934                         )->gather(1)->[0]->shortname;
935                 }
936
937                 if ($class eq '-') {
938                         $class = 'keyword';
939                 }
940
941                 return opensearch_osd($version, $lib, $class, $base);
942         }
943
944
945         my $page = $cgi->param('startPage') || 1;
946         my $offset = $cgi->param('startIndex') || 1;
947         my $limit = $cgi->param('count') || 10;
948
949         $page = 1 if ($page !~ /^\d+$/);
950         $offset = 1 if ($offset !~ /^\d+$/);
951         $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
952
953         if ($page > 1) {
954                 $offset = ($page - 1) * $limit;
955         } else {
956                 $offset -= 1;
957         }
958
959         my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
960         (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
961
962         $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
963         $lang = '' if ($lang eq '*');
964
965         $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
966         $sort ||= '';
967         $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
968         $sortdir ||= '';
969
970         $terms .= " " if ($terms && $cgi->param('searchTerms'));
971         $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
972
973         $class = $cgi->param('searchClass') if $cgi->param('searchClass');
974         $class ||= '-';
975
976         $type = $cgi->param('responseType') if $cgi->param('responseType');
977         $type ||= '-';
978
979         $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
980         $org ||= '-';
981
982
983         my $kwt = $cgi->param('kw');
984         my $tit = $cgi->param('ti');
985         my $aut = $cgi->param('au');
986         my $sut = $cgi->param('su');
987         my $set = $cgi->param('se');
988
989         $terms .= " " if ($terms && $kwt);
990         $terms .= "keyword: $kwt" if ($kwt);
991         $terms .= " " if ($terms && $tit);
992         $terms .= "title: $tit" if ($tit);
993         $terms .= " " if ($terms && $aut);
994         $terms .= "author: $aut" if ($aut);
995         $terms .= " " if ($terms && $sut);
996         $terms .= "subject: $sut" if ($sut);
997         $terms .= " " if ($terms && $set);
998         $terms .= "series: $set" if ($set);
999
1000         if ($version eq '1.0') {
1001                 $type = 'rss2';
1002         } elsif ($type eq '-') {
1003                 $type = 'atom';
1004         }
1005         my $flesh_feed = ($type =~ /-full$/o) ? 1 : 0;
1006
1007         if ($terms eq 'help') {
1008                 print $cgi->header(-type => 'text/html');
1009                 print <<"               HTML";
1010                         <html>
1011                          <head>
1012                           <title>just type something!</title>
1013                          </head>
1014                          <body>
1015                           <p>You are in a maze of dark, twisty stacks, all alike.</p>
1016                          </body>
1017                         </html>
1018                 HTML
1019                 return Apache2::Const::OK;
1020     }
1021         
1022         $terms = decode_utf8($terms);
1023         $lang = 'eng' if ($lang eq 'en-US');
1024
1025         $log->debug("OpenSearch terms: $terms");
1026
1027         my $org_unit;
1028         if ($org eq '-') {
1029                 $org_unit = $actor->request(
1030                         'open-ils.actor.org_unit_list.search' => parent_ou => undef
1031                 )->gather(1);
1032         } elsif ($org !~ /^\d+$/o) {
1033                 $org_unit = $actor->request(
1034                         'open-ils.actor.org_unit_list.search' => shortname => uc($org)
1035                 )->gather(1);
1036         } else {
1037                 $org_unit = $actor->request(
1038                         'open-ils.actor.org_unit_list.search' => id => $org
1039                 )->gather(1);
1040         }
1041
1042     my $recs = $search->request(
1043         'open-ils.search.biblio.multiclass.query' => {
1044                         org_unit        => $org_unit->[0]->id,
1045                         offset          => $offset,
1046                         limit           => $limit,
1047                         sort            => $sort,
1048                         sort_dir        => $sortdir,
1049                         ($lang ?    ( 'language' => $lang    ) : ()),
1050                 } => $terms => 1
1051         )->gather(1);
1052
1053         $log->debug("Hits for [$terms]: $recs->{count}");
1054
1055         my $feed = create_record_feed(
1056                 'record',
1057                 $type,
1058                 [ map { $_->[0] } @{$recs->{ids}} ],
1059                 $unapi,
1060                 $org,
1061                 $flesh_feed
1062         );
1063
1064         $log->debug("Feed created...");
1065
1066         $feed->root($root);
1067         $feed->lib($org);
1068         $feed->search($terms);
1069         $feed->class($class);
1070
1071         $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1072
1073         $feed->creator($host);
1074         $feed->update_ts();
1075
1076         $feed->_create_node(
1077                 $feed->{item_xpath},
1078                 'http://a9.com/-/spec/opensearch/1.1/',
1079                 'totalResults',
1080                 $recs->{count},
1081         );
1082
1083         $feed->_create_node(
1084                 $feed->{item_xpath},
1085                 'http://a9.com/-/spec/opensearch/1.1/',
1086                 'startIndex',
1087                 $offset + 1,
1088         );
1089
1090         $feed->_create_node(
1091                 $feed->{item_xpath},
1092                 'http://a9.com/-/spec/opensearch/1.1/',
1093                 'itemsPerPage',
1094                 $limit,
1095         );
1096
1097         $log->debug("...basic feed data added...");
1098
1099         $feed->link(
1100                 next =>
1101                 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1102                 'application/opensearch+xml'
1103         ) if ($offset + $limit < $recs->{count});
1104
1105         $feed->link(
1106                 previous =>
1107                 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1108                 'application/opensearch+xml'
1109         ) if ($offset);
1110
1111         $feed->link(
1112                 self =>
1113                 $base .  "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1114                 'application/opensearch+xml'
1115         );
1116
1117         $feed->link(
1118                 alternate =>
1119                 $base .  "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1120                 'application/rss+xml'
1121         );
1122
1123         $feed->link(
1124                 atom =>
1125                 $base .  "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1126                 'application/atom+xml'
1127         );
1128
1129         $feed->link(
1130                 'html' =>
1131                 $base .  "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1132                 'text/html'
1133         );
1134
1135         $feed->link(
1136                 'html-full' =>
1137                 $base .  "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1138                 'text/html'
1139         );
1140
1141         $feed->link( 'unapi-server' => $unapi);
1142
1143         $log->debug("...feed links added...");
1144
1145 #       $feed->link(
1146 #               opac =>
1147 #               $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1148 #                       join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1149 #               'text/html'
1150 #       );
1151
1152         print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1153
1154         $log->debug("...and feed returned.");
1155
1156         return Apache2::Const::OK;
1157 }
1158
1159 sub create_record_feed {
1160         my $search = shift;
1161         my $type = shift;
1162         my $records = shift;
1163         my $unapi = shift;
1164
1165         my $lib = uc(shift()) || '-';
1166         my $flesh = shift;
1167         $flesh = 1 if (!defined($flesh));
1168
1169         my $cgi = new CGI;
1170         my $base = $cgi->url;
1171         my $host = $cgi->virtual_host || $cgi->server_name;
1172
1173         my $year = (gmtime())[5] + 1900;
1174
1175         my $flesh_feed = ($type =~ s/-full$//o) ? 1 : 0;
1176
1177         my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1178         $feed->base($base) if ($flesh);
1179         $feed->unapi($unapi) if ($flesh);
1180
1181         $type = 'atom' if ($type eq 'html');
1182         $type = 'marcxml' if ($type eq 'htmlholdings');
1183
1184         #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1185
1186         my $count = 0;
1187         for my $record (@$records) {
1188                 next unless($record);
1189
1190                 #my $rec = $record->id;
1191                 my $rec = $record;
1192
1193                 my $item_tag = "tag:$host,$year:biblio-record_entry/$rec/$lib";
1194                 $item_tag = "tag:$host,$year:isbn/$rec/$lib" if ($search eq 'isbn');
1195
1196                 my $xml = $supercat->request(
1197                         "open-ils.supercat.$search.$type.retrieve",
1198                         $rec
1199                 )->gather(1);
1200                 next unless $xml;
1201
1202                 my $node = $feed->add_item($xml);
1203                 next unless $node;
1204
1205                 $xml = '';
1206                 if ($lib && $type eq 'marcxml' &&  $flesh) {
1207                         my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib );
1208                         while ( !$r->complete ) {
1209                                 $xml .= join('', map {$_->content} $r->recv);
1210                         }
1211                         $xml .= join('', map {$_->content} $r->recv);
1212                         $node->add_holdings($xml);
1213                 }
1214
1215                 $node->id($item_tag);
1216                 #$node->update_ts(clense_ISO8601($record->edit_date));
1217                 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh);
1218                 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh);
1219                 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1220                 $node->link('unapi-id' => $item_tag) if ($flesh);
1221         }
1222
1223         return $feed;
1224 }
1225
1226 sub entityize {
1227         my $stuff = NFC(shift());
1228         $stuff =~ s/&(?!\S+;)/&amp;/gso;
1229         $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
1230         return $stuff;
1231 }
1232
1233 sub string_browse {
1234         my $apache = shift;
1235         return Apache2::Const::DECLINED if (-e $apache->filename);
1236
1237         my $cgi = new CGI;
1238         my $year = (gmtime())[5] + 1900;
1239
1240         my $host = $cgi->virtual_host || $cgi->server_name;
1241
1242         my $add_path = 0;
1243         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1244                 my $rel_name = $cgi->url(-relative=>1);
1245                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1246         }
1247
1248         my $url = $cgi->url(-path_info=>$add_path);
1249         my $root = (split 'browse', $url)[0];
1250         my $base = (split 'browse', $url)[0] . 'browse';
1251         my $unapi = (split 'browse', $url)[0] . 'unapi';
1252
1253         my $path = $cgi->path_info;
1254         $path =~ s/^\///og;
1255
1256         my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1257         #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1258
1259         $site ||= $cgi->param('searchOrg');
1260         $page ||= $cgi->param('startPage') || 0;
1261         $page_size ||= $cgi->param('count') || 9;
1262
1263         $page = 0 if ($page !~ /^-?\d+$/);
1264
1265         my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1266         my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1267
1268         unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1269                 warn "something's wrong...";
1270                 warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1271                 return undef;
1272         }
1273
1274         $string = decode_utf8($string);
1275         $string =~ s/\+/ /go;
1276         $string =~ s/'//go;
1277
1278         my $tree = $supercat->request(
1279                 "open-ils.supercat.$axis.browse",
1280                 $string,
1281                 $site,
1282                 $page_size,
1283                 $page
1284         )->gather(1);
1285
1286         my ($header,$content) = $browse_types{$axis}{$format}->($tree,$prev,$next);
1287         print $header.$content;
1288         return Apache2::Const::OK;
1289 }
1290
1291 our %qualifier_map = (
1292
1293     # Som EG qualifiers
1294     'eg.site'               => 'site',
1295     'eg.sort'               => 'sort',
1296     'eg.direction'          => 'dir',
1297     'eg.available'          => 'available',
1298
1299     # Title class:
1300     'eg.title'              => 'title',
1301     'dc.title'              => 'title',
1302     'bib.titleabbreviated'  => 'title|abbreviated',
1303     'bib.titleuniform'      => 'title|uniform',
1304     'bib.titletranslated'   => 'title|translated',
1305     'bib.titlealternative'  => 'title',
1306     'bib.titleseries'       => 'series',
1307     'eg.series'             => 'title',
1308
1309     # Author/Name class:
1310     'eg.author'             => 'title',
1311     'eg.name'               => 'title',
1312     'creator'               => 'author',
1313     'dc.creator'            => 'author',
1314     'dc.contributer'        => 'author',
1315     'dc.publisher'          => 'keyword',
1316     'bib.name'              => 'author',
1317     'bib.namepersonal'      => 'author|personal',
1318     'bib.namepersonalfamily'=> 'author|personal',
1319     'bib.namepersonalgiven' => 'author|personal',
1320     'bib.namecorporate'     => 'author|corporate',
1321     'bib.nameconference'    => 'author|conference',
1322
1323     # Subject class:
1324     'eg.subject'            => 'subject',
1325     'dc.subject'            => 'subject',
1326     'bib.subjectplace'      => 'subject|geographic',
1327     'bib.subjecttitle'      => 'keyword',
1328     'bib.subjectname'       => 'subject|name',
1329     'bib.subjectoccupation' => 'keyword',
1330
1331     # Keyword class:
1332     'eg.keyword'            => 'keyword',
1333     'srw.serverchoice'      => 'keyword',
1334
1335     # Identifiers:
1336     'dc.identifier'         => 'keyword',
1337
1338     # Dates:
1339     'bib.dateissued'        => undef,
1340     'bib.datecreated'       => undef,
1341     'bib.datevalid'         => undef,
1342     'bib.datemodified'      => undef,
1343     'bib.datecopyright'     => undef,
1344
1345     # Resource Type:
1346     'dc.type'               => undef,
1347
1348     # Format:
1349     'dc.format'             => undef,
1350
1351     # Genre:
1352     'bib.genre'             => 'keyword',
1353
1354     # Target Audience:
1355     'bib.audience'          => undef,
1356
1357     # Place of Origin:
1358     'bib.originplace'       => undef,
1359
1360     # Language
1361     'dc.language'           => 'lang',
1362
1363     # Edition
1364     'bib.edition'           => 'keyword',
1365
1366     # Part:
1367     'bib.volume'            => 'keyword',
1368     'bib.issue'             => 'keyword',
1369     'bib.startpage'         => 'keyword',
1370     'bib.endpage'           => 'keyword',
1371
1372     # Issuance:
1373     'bib.issuance'          => 'keyword',
1374 );
1375
1376 our %qualifier_ids = (
1377                 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1378                 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1379                 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1380                 srw     => ''
1381 );
1382
1383 our %nested_qualifier_map = (
1384                 eg => {
1385                         site            => ['site','Evergreen Site Code (shortname)'],
1386                         sort            => ['sort','Sort on relevance, title, author, pubdate, create_date or edit_date'],
1387                         direction       => ['dir','Sort direction (asc|desc)'],
1388                         available       => ['available','Filter to availble (true|false)'],
1389                         title           => ['title'],
1390                         author          => ['author'],
1391                         name            => ['author'],
1392                         subject         => ['subject'],
1393                         keyword         => ['keyword'],
1394                         series          => ['series'],
1395                 },
1396                 dc => {
1397                         title           => ['title'],
1398                         creator         => ['author'],
1399                         contributor     => ['author'],
1400                         publisher       => ['keyword'],
1401                         subject         => ['subject'],
1402                         identifier      => ['keyword'],
1403                         type            => [undef],
1404                         format          => [undef],
1405                         language        => ['lang'],
1406                 },
1407                 bib => {
1408                 # Title class:
1409                 titleAbbreviated        => ['title'],
1410                     titleUniform                => ['title'],
1411                         titleTranslated         => ['title'],
1412                 titleAlternative        => ['title'],
1413                     titleSeries                 => ['series'],
1414
1415     # Author/Name class:
1416                         name                            => ['author'],
1417                         namePersonal            => ['author'],
1418                         namePersonalFamily      => ['author'],
1419                         namePersonalGiven       => ['author'],
1420                         nameCorporate           => ['author'],
1421                         nameConference          => ['author'],
1422
1423                 # Subject class:
1424                         subjectPlace            => ['subject'],
1425                         subjectTitle            => ['keyword'],
1426                         subjectName                     => ['subject|name'],
1427                         subjectOccupation       => ['keyword'],
1428
1429     # Keyword class:
1430
1431     # Dates:
1432                         dateIssued                      => [undef],
1433                         dateCreated                     => [undef],
1434                         dateValid                       => [undef],
1435                         dateModified            => [undef],
1436                         dateCopyright           => [undef],
1437
1438     # Genre:
1439                         genre                           => ['keyword'],
1440
1441     # Target Audience:
1442                         audience                        => [undef],
1443
1444     # Place of Origin:
1445                         originPlace                     => [undef],
1446
1447     # Edition
1448                         edition                         => ['keyword'],
1449
1450     # Part:
1451                         volume                          => ['keyword'],
1452                         issue                           => ['keyword'],
1453                         startPage                       => ['keyword'],
1454                         endPage                         => ['keyword'],
1455
1456     # Issuance:
1457                         issuance                        => ['keyword'],
1458                 },
1459                 srw     => {
1460                         serverChoice            => ['keyword'],
1461                 },
1462 );
1463
1464
1465 my $base_explain = <<XML;
1466 <explain
1467                 id="evergreen-sru-explain-full"
1468                 authoritative="true"
1469                 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1470                 xmlns="http://explain.z3950.org/dtd/2.0/">
1471         <serverInfo transport="http" protocol="SRU" version="1.1">
1472                 <host/>
1473                 <port/>
1474                 <database/>
1475         </serverInfo>
1476
1477         <databaseInfo>
1478                 <title primary="true"/>
1479                 <description primary="true"/>
1480         </databaseInfo>
1481
1482         <indexInfo>
1483                 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1484         </indexInfo>
1485
1486         <schemaInfo>
1487                 <schema
1488                                 identifier="info:srw/schema/1/marcxml-v1.1"
1489                                 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1490                                 sort="true"
1491                                 retrieve="true"
1492                                 name="marcxml">
1493                         <title>MARC21Slim (marcxml)</title>
1494                 </schema>
1495         </schemaInfo>
1496
1497         <configInfo>
1498                 <default type="numberOfRecords">10</default>
1499                 <default type="contextSet">eg</default>
1500                 <default type="index">keyword</default>
1501                 <default type="relation">all</default>
1502                 <default type="sortSchema">marcxml</default>
1503                 <default type="retrieveSchema">marcxml</default>
1504                 <setting type="maximumRecords">10</setting>
1505                 <supports type="relationModifier">relevant</supports>
1506                 <supports type="relationModifier">stem</supports>
1507                 <supports type="relationModifier">fuzzy</supports>
1508                 <supports type="relationModifier">word</supports>
1509         </configInfo>
1510
1511 </explain>
1512 XML
1513
1514
1515 my $ex_doc;
1516 sub sru_search {
1517     my $cgi = new CGI;
1518
1519     my $req = SRU::Request->newFromCGI( $cgi );
1520     my $resp = SRU::Response->newFromRequest( $req );
1521
1522     if ( $resp->type eq 'searchRetrieve' ) {
1523                 my $cql_query = $req->query;
1524                 my $search_string = $req->cql->toEvergreen;
1525
1526         my $offset = $req->startRecord;
1527         $offset-- if ($offset);
1528         $offset ||= 0;
1529
1530         my $limit = $req->maximumRecords;
1531         $limit ||= 10;
1532
1533         warn "SRU search string [$cql_query] converted to [$search_string]\n";
1534
1535                 my $recs = $search->request(
1536                         'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1537                 )->gather(1);
1538
1539         my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1540
1541         $resp->addRecord(
1542             SRU::Response::Record->new(
1543                 recordSchema    => 'info:srw/schema/1/marcxml-v1.1',
1544                 recordData => $_->marc
1545             )
1546         ) for @$bre;
1547
1548         $resp->numberOfRecords($recs->{count});
1549
1550     } elsif ( $resp->type eq 'explain' ) {
1551                 if (!$ex_doc) {
1552                         my $host = $cgi->virtual_host || $cgi->server_name;
1553
1554                         my $add_path = 0;
1555                         if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1556                                 my $rel_name = $cgi->url(-relative=>1);
1557                                 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1558                         }
1559                         my $base = $cgi->url(-base=>1);
1560                         my $url = $cgi->url(-path_info=>$add_path);
1561                         $url =~ s/^$base\///o;
1562
1563                         my $doc = $parser->parse_string($base_explain);
1564                         my $e = $doc->documentElement;
1565                         $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
1566                         $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
1567                         $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
1568
1569                         for my $name ( keys %OpenILS::WWW::SuperCat::nested_qualifier_map ) {
1570
1571                                 my $identifier = $OpenILS::WWW::SuperCat::qualifier_ids{ $name };
1572
1573                                 next unless $identifier;
1574
1575                                 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
1576                                 $set_node->setAttribute( identifier => $identifier );
1577                                 $set_node->setAttribute( name => $name );
1578
1579                                 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
1580
1581                                 for my $index ( keys %{ $OpenILS::WWW::SuperCat::nested_qualifier_map{$name} } ) {
1582                                         my $desc = $OpenILS::WWW::SuperCat::nested_qualifier_map{$name}{$index}[1] || $index;
1583
1584                                         my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
1585
1586                                         my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
1587                                         $map_node->appendChild( $name_node );
1588
1589                                         my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
1590
1591                                         my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
1592                                         $index_node->appendChild( $title_node );
1593                                         $index_node->appendChild( $map_node );
1594
1595                                         $index_node->setAttribute( id => $name . '.' . $index );
1596                                         $title_node->appendText( $desc );
1597                                         $name_node->setAttribute( set => $name );
1598                                         $name_node->appendText($index );
1599
1600                                         $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
1601                                 }
1602                         }
1603
1604                         $ex_doc = $e->toString;
1605                 }
1606
1607                 $resp->record(
1608                         SRU::Response::Record->new(
1609                                 recordSchema    => 'info:srw/cql-context-set/2/zeerex-1.1',
1610                                 recordData              => $ex_doc
1611                         )
1612                 );
1613         }
1614
1615         print $cgi->header( -type => 'application/xml' );
1616         print entityize($resp->asXML) . "\n";
1617     return Apache2::Const::OK;
1618 }
1619
1620
1621 {
1622     package CQL::BooleanNode;
1623
1624     sub toEvergreen {
1625         my $self     = shift;
1626         my $left     = $self->left();
1627         my $right    = $self->right();
1628         my $leftStr  = $left->toEvergreen;
1629         my $rightStr = $right->toEvergreen();
1630
1631         my $op =  '||' if uc $self->op() eq 'OR';
1632         $op ||=  '&&';
1633
1634         return  "$leftStr $rightStr";
1635     }
1636
1637     package CQL::TermNode;
1638
1639     sub toEvergreen {
1640         my $self      = shift;
1641         my $qualifier = $self->getQualifier();
1642         my $term      = $self->getTerm();
1643         my $relation  = $self->getRelation();
1644
1645         my $query;
1646         if ( $qualifier ) {
1647                         my ($qset, $qname) = split(/\./, $qualifier);
1648
1649                         warn "!!! $qset, $qname   $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0]\n";
1650
1651             if ( exists($OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}) ) {
1652                 $qualifier = $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0] || 'kw';
1653                         }
1654
1655             my @modifiers = $relation->getModifiers();
1656
1657             my $base = $relation->getBase();
1658             if ( grep { $base eq $_ } qw/= scr exact all/ ) {
1659
1660                 my $quote_it = 1;
1661                 foreach my $m ( @modifiers ) {
1662                     if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
1663                         $quote_it = 0;
1664                         last;
1665                     }
1666                 }
1667
1668                 $quote_it = 0 if ( $base eq 'all' );
1669                 $term = maybeQuote($term) if $quote_it;
1670
1671             } else {
1672                 croak( "Evergreen doesn't support the $base relations" );
1673             }
1674
1675
1676         } else {
1677             $qualifier = "kw";
1678         }
1679
1680         return "$qualifier:$term";
1681     }
1682 }
1683
1684 1;