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