1 package OpenILS::WWW::SuperCat;
2 use strict; use warnings;
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;
15 use OpenSRF::EX qw(:try);
16 use OpenSRF::Utils qw/:datetime/;
17 use OpenSRF::Utils::Cache;
19 use OpenSRF::AppSession;
24 use Unicode::Normalize;
25 use OpenILS::Utils::Fieldmapper;
26 use OpenILS::WWW::SuperCat::Feed;
27 use OpenSRF::Utils::Logger qw/$logger/;
28 use OpenILS::Application::AppUtils;
33 my $log = 'OpenSRF::Utils::Logger';
34 my $U = 'OpenILS::Application::AppUtils';
36 # set the bootstrap config when this module is loaded
37 my ($bootstrap, $supercat, $actor, $parser, $search, $xslt, $cn_browse_xslt, %browse_types, %qualifier_map);
39 $browse_types{call_number}{xml} = sub {
42 my $year = (gmtime())[5] + 1900;
45 $content .= "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>\n";
48 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
49 $cn_class =~ s/Fieldmapper-//gso;
51 my $cn_tag = "tag:open-ils.org,$year:$cn_class/".$cn->id;
52 my $cn_lib = $cn->owning_lib->shortname;
53 my $cn_label = $cn->label;
55 $cn_label =~ s/\n//gos;
56 $cn_label =~ s/&/&/go;
57 $cn_label =~ s/'/'/go;
58 $cn_label =~ s/</</go;
59 $cn_label =~ s/>/>/go;
61 (my $ou_class = $cn->owning_lib->class_name) =~ s/::/-/gso;
62 $ou_class =~ s/Fieldmapper-//gso;
64 my $ou_tag = "tag:open-ils.org,$year:$ou_class/".$cn->owning_lib->id;
65 my $ou_name = $cn->owning_lib->name;
67 $ou_name =~ s/\n//gos;
68 $ou_name =~ s/'/'/go;
70 (my $rec_class = $cn->record->class_name) =~ s/::/-/gso;
71 $rec_class =~ s/Fieldmapper-//gso;
73 my $rec_tag = "tag:open-ils.org,$year:$rec_class/".$cn->record->id.'/'.$cn->owning_lib->shortname;
75 $content .= "<volume id='$cn_tag' lib='$cn_lib' label='$cn_label'>\n";
76 $content .= "<owning_lib xmlns='http://open-ils.org/spec/actors/v1' id='$ou_tag' name='$ou_name'/>\n";
78 my $r_doc = $parser->parse_string($cn->record->marc);
79 $r_doc->documentElement->setAttribute( id => $rec_tag );
80 $content .= $U->entityize($r_doc->documentElement->toString);
82 $content .= "</volume>\n";
85 $content .= "</volumes>\n";
86 return ("Content-type: application/xml\n\n",$content);
90 $browse_types{call_number}{html} = sub {
95 if (!$cn_browse_xslt) {
96 $cn_browse_xslt = $parser->parse_file(
97 OpenSRF::Utils::SettingsClient
99 ->config_value( dirs => 'xsl' ).
102 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
105 my (undef,$xml) = $browse_types{call_number}{xml}->($tree);
108 "Content-type: text/html\n\n",
110 $cn_browse_xslt->transform(
111 $parser->parse_string( $xml ),
126 OpenSRF::System->bootstrap_client( config_file => $bootstrap );
128 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
129 Fieldmapper->import(IDL => $idl);
131 $supercat = OpenSRF::AppSession->create('open-ils.supercat');
132 $actor = OpenSRF::AppSession->create('open-ils.actor');
133 $search = OpenSRF::AppSession->create('open-ils.search');
134 $parser = new XML::LibXML;
135 $xslt = new XML::LibXSLT;
137 $cn_browse_xslt = $parser->parse_file(
138 OpenSRF::Utils::SettingsClient
140 ->config_value( dirs => 'xsl' ).
144 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
146 %qualifier_map = %{$supercat
147 ->request("open-ils.supercat.biblio.search_aliases")
150 # Append the non-search-alias attributes to the qualifier map
151 push(@{$qualifier_map{'eg'}}, qw/
167 preferred_language_weight
168 preferred_language_multiplier
172 ->request("open-ils.supercat.record.formats")
175 $list = [ map { (keys %$_)[0] } @$list ];
176 push @$list, 'htmlholdings','html', 'marctxt', 'ris';
178 for my $browse_axis ( qw/title author subject topic series item-age/ ) {
179 for my $record_browse_format ( @$list ) {
181 my $__f = $record_browse_format;
182 my $__a = $browse_axis;
184 $browse_types{$__a}{$__f} = sub {
185 my $record_list = shift;
188 my $real_format = shift || $__f;
193 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
194 my $feed = create_record_feed( 'record', $real_format, $record_list, $unapi, $site, undef, $real_format =~ /(-full|-uris)$/o ? 1 : 0 );
195 $feed->root( "$base/../" );
197 $feed->link( next => $next => $feed->type );
198 $feed->link( previous => $prev => $feed->type );
201 "Content-type: ". $feed->type ."; charset=utf-8\n\n",
209 for my $basic_axis ( qw/authority.title authority.author authority.subject authority.topic/ ) {
210 for my $browse_axis ( ($basic_axis, $basic_axis . ".refs") ) {
213 my $__a = $browse_axis;
215 $browse_types{$__a}{$__f} = sub {
216 my $record_list = shift;
219 my $real_format = shift || $__f;
224 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
225 my $feed = create_record_feed( 'authority', $real_format, $record_list, $unapi, $site, undef, $real_format =~ /-full$/o ? -1 : 0 );
226 $feed->root( "$base/../" );
227 $feed->link( next => $next => $feed->type );
228 $feed->link( previous => $prev => $feed->type );
231 "Content-type: ". $feed->type ."; charset=utf-8\n\n",
240 =head2 parse_feed_type($type)
242 Determines whether and how a given feed type needs to be "fleshed out"
243 with holdings information.
245 The feed type could end with the string "-full", in which case we want
246 to return call numbers, copies, and URIS.
248 Or the feed type could be "-uris", in which case we want to return
249 call numbers and URIS.
251 Otherwise, we won't return any holdings.
255 sub parse_feed_type {
258 if ($type =~ /-full$/o) {
262 if ($type =~ /-uris$/o) {
266 # Otherwise, we'll return just the facts, ma'am
270 =head2 supercat_format($format_hashref, $format_type)
272 Given a reference to a hash containing the namespace_uri,
273 docs, and schema location attributes for a set of formats,
274 generate the XML description required by the supercat service.
276 We derive the base type from the format type so that we do not
277 have to populate the hash with redundant information.
281 sub supercat_format {
285 (my $base_type = $type) =~ s/(-full|-uris)$//o;
287 my $format = "<format><name>$type</name><type>application/xml</type>";
289 for my $part ( qw/namespace_uri docs schema_location/ ) {
290 $format .= "<$part>$$h{$base_type}{$part}</$part>"
291 if ($$h{$base_type}{$part});
294 $format .= '</format>';
299 =head2 unapi_format($format_hashref, $format_type)
301 Given a reference to a hash containing the namespace_uri,
302 docs, and schema location attributes for a set of formats,
303 generate the XML description required by the supercat service.
305 We derive the base type from the format type so that we do not
306 have to populate the hash with redundant information.
314 (my $base_type = $type) =~ s/(-full|-uris)$//o;
316 my $format = "<format name='$type' type='application/xml'";
318 for my $part ( qw/namespace_uri docs schema_location/ ) {
319 $format .= " $part='$$h{$base_type}{$part}'"
320 if ($$h{$base_type}{$part});
332 return Apache2::Const::DECLINED if (-e $apache->filename);
334 (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
337 ->request("open-ils.supercat.oisbn", $isbn)
340 print "Content-type: application/xml; charset=utf-8\n\n";
341 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
343 unless (exists $$list{metarecord}) {
345 return Apache2::Const::OK;
348 print "<idlist metarecord='$$list{metarecord}'>\n";
350 for ( keys %{ $$list{record_list} } ) {
351 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
352 print " <isbn record='$_'>$o</isbn>\n"
357 return Apache2::Const::OK;
363 return Apache2::Const::DECLINED if (-e $apache->filename);
368 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
369 my $rel_name = $cgi->url(-relative=>1);
370 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
373 my $url = $cgi->url(-path_info=>$add_path);
374 my $root = (split 'unapi', $url)[0];
375 my $base = (split 'unapi', $url)[0] . 'unapi';
378 my $uri = $cgi->param('id') || '';
379 my $host = $cgi->virtual_host || $cgi->server_name;
381 my $skin = $cgi->param('skin') || 'default';
382 my $locale = $cgi->param('locale') || 'en-US';
384 # Enable localized results of copy status, etc
385 $supercat->session_locale($locale);
387 my $format = $cgi->param('format');
388 my $flesh_feed = parse_feed_type($format);
389 (my $base_format = $format) =~ s/(-full|-uris)$//o;
390 my ($id,$type,$command,$lib,$depth,$paging) = ('','','');
393 my $body = "Content-type: application/xml; charset=utf-8\n\n";
395 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
398 ($lib,$depth) = split('/', $4);
400 $type = 'metarecord' if ($1 =~ /^m/o);
401 $type = 'authority' if ($1 =~ /^authority/o);
404 ->request("open-ils.supercat.$type.formats")
407 if ($type eq 'record' or $type eq 'isbn') {
408 $body .= <<" FORMATS";
410 <format name='opac' type='text/html'/>
411 <format name='html' type='text/html'/>
412 <format name='htmlholdings' type='text/html'/>
413 <format name='holdings_xml' type='application/xml'/>
414 <format name='holdings_xml-full' type='application/xml'/>
415 <format name='html-full' type='text/html'/>
416 <format name='htmlholdings-full' type='text/html'/>
417 <format name='marctxt' type='text/plain'/>
418 <format name='ris' type='text/plain'/>
420 } elsif ($type eq 'metarecord') {
421 $body .= <<" FORMATS";
423 <format name='opac' type='text/html'/>
426 $body .= <<" FORMATS";
432 my ($type) = keys %$h;
433 $body .= unapi_format($h, $type);
435 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
436 $body .= unapi_format($h, "$type-full");
437 $body .= unapi_format($h, "$type-uris");
441 $body .= "</formats>\n";
445 ->request("open-ils.supercat.$type.formats")
450 ->request("open-ils.supercat.metarecord.formats")
454 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
455 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
457 $body .= <<" FORMATS";
459 <format name='opac' type='text/html'/>
460 <format name='html' type='text/html'/>
461 <format name='htmlholdings' type='text/html'/>
462 <format name='holdings_xml' type='application/xml'/>
463 <format name='holdings_xml-full' type='application/xml'/>
464 <format name='html-full' type='text/html'/>
465 <format name='htmlholdings-full' type='text/html'/>
466 <format name='marctxt' type='text/plain'/>
467 <format name='ris' type='text/plain'/>
472 my ($type) = keys %$h;
473 $body .= "\t" . unapi_format($h, $type);
475 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
476 $body .= "\t" . unapi_format($h, "$type-full");
477 $body .= "\t" . unapi_format($h, "$type-uris");
481 $body .= "</formats>\n";
485 return Apache2::Const::OK;
489 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
493 ($lib,$depth) = split('/', $4);
495 $type = 'metarecord' if ($scheme =~ /^metabib/o);
496 $type = 'isbn' if ($scheme =~ /^isbn/o);
497 $type = 'acp' if ($scheme =~ /^asset-copy/o);
498 $type = 'acn' if ($scheme =~ /^asset-call_number/o);
499 $type = 'auri' if ($scheme =~ /^asset-uri/o);
500 $type = 'authority' if ($scheme =~ /^authority/o);
501 $command = 'retrieve';
502 $command = 'browse' if (grep { $scheme eq $_ } qw/call_number title author subject topic authority.title authority.author authority.subject authority.topic series item-age/);
506 $paging = [split ',', $paging];
511 if (!$lib || $lib eq '-') {
512 $lib = $actor->request(
513 'open-ils.actor.org_unit_list.search' => parent_ou => undef
514 )->gather(1)->[0]->shortname;
517 my ($lib_object,$lib_id,$ou_types,$lib_depth);
518 if ($type ne 'acn' && $type ne 'acp' && $type ne 'auri') {
519 $lib_object = $actor->request(
520 'open-ils.actor.org_unit_list.search' => shortname => $lib
522 $lib_id = $lib_object->id;
524 $ou_types = $actor->request( 'open-ils.actor.org_types.retrieve' )->gather(1);
525 $lib_depth = $depth || (grep { $_->id == $lib_object->ou_type } @$ou_types)[0]->depth;
528 if ($command eq 'browse') {
529 print "Location: $root/browse/$base_format/$scheme/$lib/$id\n\n";
533 if ($type eq 'isbn') {
534 my $rec = $supercat->request('open-ils.supercat.isbn.object.retrieve',$id)->gather(1);
536 print "Content-type: text/html; charset=utf-8\n\n";
537 $apache->custom_response( 404, <<" HTML");
540 <title>Type [$type] with id [$id] not found!</title>
544 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
555 { (keys(%$_))[0] eq $base_format }
556 @{ $supercat->request("open-ils.supercat.$type.formats")->gather(1) }
558 { $_ eq $base_format }
559 qw/opac html htmlholdings marctxt ris holdings_xml/
561 print "Content-type: text/html; charset=utf-8\n\n";
562 $apache->custom_response( 406, <<" HTML");
565 <title>Invalid format [$format] for type [$type]!</title>
569 <center>Sorry, format $format is not valid for type $type.</center>
576 if ($format eq 'opac') {
577 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id&l=$lib_id&d=$lib_depth\n\n"
578 if ($type eq 'metarecord');
579 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id&l=$lib_id&d=$lib_depth\n\n"
580 if ($type eq 'record');
582 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format) && ($type ne 'acn' && $type ne 'acp' && $type ne 'auri')) {
583 my $feed = create_record_feed(
594 print "Content-type: text/html; charset=utf-8\n\n";
595 $apache->custom_response( 404, <<" HTML");
598 <title>Type [$type] with id [$id] not found!</title>
602 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
610 $feed->creator($host);
612 $feed->link( unapi => $base) if ($flesh_feed);
614 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
615 print $U->entityize($feed->toString) . "\n";
617 return Apache2::Const::OK;
620 my $method = "open-ils.supercat.$type.$base_format.$command";
622 push @params, $lib, $lib_depth, $flesh_feed, $paging if ($base_format eq 'holdings_xml');
624 # for acn, acp, etc, the "lib" pathinfo position isn't useful.
625 # however, we can have it carry extra options like no_record! (comma separated)
626 push @params, { map { ( $_ => 1 ) } split(',', $lib) } if ( grep { $type eq $_} qw/acn acp auri/);
628 my $req = $supercat->request($method,@params);
629 my $data = $req->gather();
631 if ($req->failed || !$data) {
632 print "Content-type: text/html; charset=utf-8\n\n";
633 $apache->custom_response( 404, <<" HTML");
636 <title>$type $id not found!</title>
640 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
647 print "Content-type: application/xml; charset=utf-8\n\n$data";
649 if ($base_format eq 'holdings_xml') {
650 while (my $c = $req->recv) {
655 return Apache2::Const::OK;
661 return Apache2::Const::DECLINED if (-e $apache->filename);
666 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
667 my $rel_name = $cgi->url(-relative=>1);
668 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
671 my $url = $cgi->url(-path_info=>$add_path);
672 my $root = (split 'supercat', $url)[0];
673 my $base = (split 'supercat', $url)[0] . 'supercat';
674 my $unapi = (split 'supercat', $url)[0] . 'unapi';
676 my $host = $cgi->virtual_host || $cgi->server_name;
678 my $path = $cgi->path_info;
679 my ($id,$type,$format,$command) = reverse split '/', $path;
680 my $flesh_feed = parse_feed_type($format);
681 (my $base_format = $format) =~ s/(-full|-uris)$//o;
683 my $skin = $cgi->param('skin') || 'default';
684 my $locale = $cgi->param('locale') || 'en-US';
686 # Enable localized results of copy status, etc
687 $supercat->session_locale($locale);
689 if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
690 print "Content-type: application/xml; charset=utf-8\n";
693 ->request("open-ils.supercat.$1.formats")
701 <type>text/html</type>
704 if ($1 eq 'record' or $1 eq 'isbn') {
706 <name>htmlholdings</name>
707 <type>text/html</type>
711 <type>text/html</type>
714 <name>htmlholdings-full</name>
715 <type>text/html</type>
718 <name>html-full</name>
719 <type>text/html</type>
723 <type>text/plain</type>
727 <type>text/plain</type>
732 my ($type) = keys %$h;
733 print supercat_format($h, $type);
735 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
736 print supercat_format($h, "$type-full");
737 print supercat_format($h, "$type-uris");
742 print "</formats>\n";
744 return Apache2::Const::OK;
748 ->request("open-ils.supercat.record.formats")
753 ->request("open-ils.supercat.metarecord.formats")
757 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
758 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
763 <type>text/html</type>
766 <name>htmlholdings</name>
767 <type>text/html</type>
771 <type>text/html</type>
774 <name>htmlholdings-full</name>
775 <type>text/html</type>
778 <name>html-full</name>
779 <type>text/html</type>
783 <type>text/plain</type>
787 <type>text/plain</type>
791 my ($type) = keys %$h;
792 print supercat_format($h, $type);
794 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
795 print supercat_format($h, "$type-full");
796 print supercat_format($h, "$type-uris");
801 print "</formats>\n";
804 return Apache2::Const::OK;
807 if ($format eq 'opac') {
808 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id\n\n"
809 if ($type eq 'metarecord');
810 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id\n\n"
811 if ($type eq 'record');
814 } elsif ($base_format eq 'marc21') {
818 my $bib = $supercat->request( "open-ils.supercat.record.object.retrieve", $id )->gather(1)->[0];
820 print "Content-type: application/octet-stream\n\n" . MARC::Record->new_from_xml( $bib->marc, 'UTF-8', 'USMARC' )->as_usmarc;
825 print "Content-type: text/html; charset=utf-8\n\n";
826 $apache->custom_response( 404, <<" HTML");
833 <center>Couldn't fetch $id as MARC21.</center>
840 return Apache2::Const::OK;
842 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
843 my $feed = create_record_feed(
851 $feed->creator($host);
855 $feed->link( unapi => $base) if ($flesh_feed);
857 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
858 print $U->entityize($feed->toString) . "\n";
860 return Apache2::Const::OK;
863 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
867 print "Content-type: text/html; charset=utf-8\n\n";
868 $apache->custom_response( 404, <<" HTML");
871 <title>$type $id not found!</title>
875 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
882 print "Content-type: application/xml; charset=utf-8\n\n";
883 print $U->entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
885 return Apache2::Const::OK;
891 return Apache2::Const::DECLINED if (-e $apache->filename);
895 my $year = (gmtime())[5] + 1900;
896 my $host = $cgi->virtual_host || $cgi->server_name;
899 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
900 my $rel_name = $cgi->url(-relative=>1);
901 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
904 my $url = $cgi->url(-path_info=>$add_path);
905 my $root = (split 'feed', $url)[0] . '/';
906 my $base = (split 'bookbag', $url)[0] . '/bookbag';
907 my $unapi = (split 'feed', $url)[0] . '/unapi';
909 my $skin = $cgi->param('skin') || 'default';
910 my $locale = $cgi->param('locale') || 'en-US';
911 my $org = $cgi->param('searchOrg');
913 # Enable localized results of copy status, etc
914 $supercat->session_locale($locale);
916 my $org_unit = get_ou($org);
917 my $scope = "l=" . $org_unit->[0]->id . "&";
919 $root =~ s{(?<!http:)//}{/}go;
920 $base =~ s{(?<!http:)//}{/}go;
921 $unapi =~ s{(?<!http:)//}{/}go;
923 my $path = $cgi->path_info;
924 #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
926 my ($id,$type) = reverse split '/', $path;
927 my $flesh_feed = parse_feed_type($type);
929 my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
930 return Apache2::Const::NOT_FOUND unless($bucket);
932 my $bucket_tag = "tag:$host,$year:record_bucket/$id";
933 if ($type eq 'opac') {
934 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
935 join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
940 my $feed = create_record_feed(
943 [ map { $_->target_biblio_record_entry } @{ $bucket->items } ],
945 $org_unit->[0]->shortname,
950 $feed->id($bucket_tag);
952 $feed->title("Items in Book Bag [".$bucket->name."]");
953 $feed->creator($host);
956 $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
957 $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
958 $feed->link(html => $base . "/html-full/$id" => 'text/html');
959 $feed->link(unapi => $unapi);
963 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
964 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
969 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
970 print $U->entityize($feed->toString) . "\n";
972 return Apache2::Const::OK;
977 return Apache2::Const::DECLINED if (-e $apache->filename);
981 my $year = (gmtime())[5] + 1900;
982 my $host = $cgi->virtual_host || $cgi->server_name;
985 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
986 my $rel_name = $cgi->url(-relative=>1);
987 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
990 my $url = $cgi->url(-path_info=>$add_path);
991 my $root = (split 'feed', $url)[0];
992 my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
993 my $unapi = (split 'feed', $url)[0] . 'unapi';
995 my $skin = $cgi->param('skin') || 'default';
996 my $locale = $cgi->param('locale') || 'en-US';
997 my $org = $cgi->param('searchOrg');
999 # Enable localized results of copy status, etc
1000 $supercat->session_locale($locale);
1002 my $org_unit = get_ou($org);
1003 my $scope = "l=" . $org_unit->[0]->id . "&";
1005 my $path = $cgi->path_info;
1006 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1008 $path =~ s/^\/(?:feed\/)?freshmeat\///og;
1010 my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
1011 my $flesh_feed = parse_feed_type($type);
1014 $limit = 10 if $limit !~ /^\d+$/;
1016 my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
1018 #if ($type eq 'opac') {
1019 # print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
1020 # join('&', map { "rl=" . $_ } @$list) .
1025 my $search = 'record';
1026 if ($rtype eq 'authority') {
1027 $search = 'authority';
1029 my $feed = create_record_feed( $search, $type, $list, $unapi, $org_unit->[0]->shortname, undef, $flesh_feed);
1033 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
1035 $feed->title("$limit most recent $rtype ${axis}s");
1038 $feed->creator($host);
1041 $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
1042 $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
1043 $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
1044 $feed->link(unapi => $unapi);
1048 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1049 join('&', map { 'rl=' . $_} @$list ),
1054 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1055 print $U->entityize($feed->toString) . "\n";
1057 return Apache2::Const::OK;
1060 sub opensearch_osd {
1061 my $version = shift;
1066 if ($version eq '1.0') {
1068 Content-type: application/opensearchdescription+xml; charset=utf-8
1070 <?xml version="1.0" encoding="UTF-8"?>
1071 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
1072 <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&startPage={startPage}&startIndex={startIndex}&count={count}</Url>
1073 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
1074 <ShortName>$lib</ShortName>
1075 <LongName>Search $lib</LongName>
1076 <Description>Search the $lib OPAC by $class.</Description>
1077 <Tags>$lib book library</Tags>
1078 <SampleSearch>harry+potter</SampleSearch>
1079 <Developer>Mike Rylander for GPLS/PINES</Developer>
1080 <Contact>feedback\@open-ils.org</Contact>
1081 <SyndicationRight>open</SyndicationRight>
1082 <AdultContent>false</AdultContent>
1083 </OpenSearchDescription>
1087 Content-type: application/opensearchdescription+xml; charset=utf-8
1089 <?xml version="1.0" encoding="UTF-8"?>
1090 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
1091 <ShortName>$lib</ShortName>
1092 <Description>Search the $lib OPAC by $class.</Description>
1093 <Tags>$lib book library</Tags>
1094 <Url type="application/rss+xml"
1095 template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1096 <Url type="application/atom+xml"
1097 template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1098 <Url type="application/x-mods3+xml"
1099 template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1100 <Url type="application/x-mods+xml"
1101 template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1102 <Url type="application/x-marcxml+xml"
1103 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1104 <Url type="text/html"
1105 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1106 <LongName>Search $lib</LongName>
1107 <Query role="example" searchTerms="harry+potter" />
1108 <Developer>Mike Rylander for GPLS/PINES</Developer>
1109 <Contact>feedback\@open-ils.org</Contact>
1110 <SyndicationRight>open</SyndicationRight>
1111 <AdultContent>false</AdultContent>
1112 <Language>en-US</Language>
1113 <OutputEncoding>UTF-8</OutputEncoding>
1114 <InputEncoding>UTF-8</InputEncoding>
1115 </OpenSearchDescription>
1119 return Apache2::Const::OK;
1122 sub opensearch_feed {
1124 return Apache2::Const::DECLINED if (-e $apache->filename);
1127 my $year = (gmtime())[5] + 1900;
1129 my $host = $cgi->virtual_host || $cgi->server_name;
1132 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1133 my $rel_name = $cgi->url(-relative=>1);
1134 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1137 my $url = $cgi->url(-path_info=>$add_path);
1138 my $root = (split 'opensearch', $url)[0];
1139 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1140 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1142 my $path = $cgi->path_info;
1143 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1145 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1151 if (!$lib || $lib eq '-') {
1152 $lib = $actor->request(
1153 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1154 )->gather(1)->[0]->shortname;
1157 if ($class eq '-') {
1161 return opensearch_osd($version, $lib, $class, $base);
1165 my $page = $cgi->param('startPage') || 1;
1166 my $offset = $cgi->param('startIndex') || 1;
1167 my $limit = $cgi->param('count') || 10;
1169 $page = 1 if ($page !~ /^\d+$/);
1170 $offset = 1 if ($offset !~ /^\d+$/);
1171 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1174 $offset = ($page - 1) * $limit;
1179 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1180 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1182 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1183 $lang = '' if ($lang eq '*');
1185 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1187 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1190 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1191 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1193 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1196 $type = $cgi->param('responseType') if $cgi->param('responseType');
1199 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1203 my $kwt = $cgi->param('kw');
1204 my $tit = $cgi->param('ti');
1205 my $aut = $cgi->param('au');
1206 my $sut = $cgi->param('su');
1207 my $set = $cgi->param('se');
1209 $terms .= " " if ($terms && $kwt);
1210 $terms .= "keyword: $kwt" if ($kwt);
1211 $terms .= " " if ($terms && $tit);
1212 $terms .= "title: $tit" if ($tit);
1213 $terms .= " " if ($terms && $aut);
1214 $terms .= "author: $aut" if ($aut);
1215 $terms .= " " if ($terms && $sut);
1216 $terms .= "subject: $sut" if ($sut);
1217 $terms .= " " if ($terms && $set);
1218 $terms .= "series: $set" if ($set);
1220 if ($version eq '1.0') {
1222 } elsif ($type eq '-') {
1225 my $flesh_feed = parse_feed_type($type);
1227 $terms = decode_utf8($terms);
1228 $lang = 'eng' if ($lang eq 'en-US');
1230 $log->debug("OpenSearch terms: $terms");
1232 my $org_unit = get_ou($org);
1234 # Apostrophes break search and get indexed as spaces anyway
1235 my $safe_terms = $terms;
1236 $safe_terms =~ s{'}{ }go;
1238 my $recs = $search->request(
1239 'open-ils.search.biblio.multiclass.query' => {
1240 org_unit => $org_unit->[0]->id,
1244 sort_dir => $sortdir,
1245 default_class => $class,
1246 ($lang ? ( 'language' => $lang ) : ()),
1247 } => $safe_terms => 1
1250 $log->debug("Hits for [$terms]: $recs->{count}");
1252 my $feed = create_record_feed(
1255 [ map { $_->[0] } @{$recs->{ids}} ],
1262 $log->debug("Feed created...");
1266 $feed->search($safe_terms);
1267 $feed->class($class);
1269 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1271 $feed->creator($host);
1274 $feed->_create_node(
1275 $feed->{item_xpath},
1276 'http://a9.com/-/spec/opensearch/1.1/',
1281 $feed->_create_node(
1282 $feed->{item_xpath},
1283 'http://a9.com/-/spec/opensearch/1.1/',
1288 $feed->_create_node(
1289 $feed->{item_xpath},
1290 'http://a9.com/-/spec/opensearch/1.1/',
1295 $log->debug("...basic feed data added...");
1299 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1300 'application/opensearch+xml'
1301 ) if ($offset + $limit < $recs->{count});
1305 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1306 'application/opensearch+xml'
1311 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1312 'application/opensearch+xml'
1317 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1318 'application/rss+xml'
1323 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1324 'application/atom+xml'
1329 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1335 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1339 $feed->link( 'unapi-server' => $unapi);
1341 $log->debug("...feed links added...");
1345 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1346 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1350 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1351 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1353 $log->debug("...and feed returned.");
1355 return Apache2::Const::OK;
1358 sub create_record_feed {
1361 my $records = shift;
1364 my $lib = uc(shift()) || '-';
1371 my $base = $cgi->url;
1372 my $host = $cgi->virtual_host || $cgi->server_name;
1374 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
1378 my $tag_prefix = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d", $month, $day);
1380 my $flesh_feed = defined($flesh) ? $flesh : parse_feed_type($type);
1382 $type =~ s/(-full|-uris)$//o;
1384 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1385 $feed->base($base) if ($flesh);
1386 $feed->unapi($unapi) if ($flesh);
1388 $type = 'atom' if ($type eq 'html');
1389 $type = 'marcxml' if (($type eq 'htmlholdings') || ($type eq 'marctxt') || ($type eq 'ris'));
1391 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1394 for my $record (@$records) {
1395 next unless($record);
1397 #my $rec = $record->id;
1400 my $item_tag = "$tag_prefix:biblio-record_entry/$rec/$lib";
1401 $item_tag = "$tag_prefix:metabib-metarecord/$rec/$lib" if ($search eq 'metarecord');
1402 $item_tag = "$tag_prefix:isbn/$rec/$lib" if ($search eq 'isbn');
1403 $item_tag .= "/$depth" if (defined($depth));
1405 $item_tag = "$tag_prefix:authority-record_entry/$rec" if ($search eq 'authority');
1407 my $xml = $supercat->request(
1408 "open-ils.supercat.$search.$type.retrieve",
1413 my $node = $feed->add_item($xml);
1417 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && $flesh > 0) {
1418 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
1419 while ( !$r->complete ) {
1420 $xml .= join('', map {$_->content} $r->recv);
1422 $xml .= join('', map {$_->content} $r->recv);
1423 $node->add_holdings($xml);
1426 $node->id($item_tag);
1427 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1428 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
1429 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
1430 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1431 $node->link('unapi-id' => $item_tag) if ($flesh);
1439 return Apache2::Const::DECLINED if (-e $apache->filename);
1442 my $year = (gmtime())[5] + 1900;
1444 my $host = $cgi->virtual_host || $cgi->server_name;
1447 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1448 my $rel_name = $cgi->url(-relative=>1);
1449 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1452 my $url = $cgi->url(-path_info=>$add_path);
1453 my $root = (split 'browse', $url)[0];
1454 my $base = (split 'browse', $url)[0] . 'browse';
1455 my $unapi = (split 'browse', $url)[0] . 'unapi';
1457 my $path = $cgi->path_info;
1460 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1461 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1463 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1465 my $status = [$cgi->param('status')];
1466 my $cpLoc = [$cgi->param('copyLocation')];
1467 $site ||= $cgi->param('searchOrg');
1468 $page ||= $cgi->param('startPage') || 0;
1469 $page_size ||= $cgi->param('count') || 9;
1471 $page = 0 if ($page !~ /^-?\d+$/);
1472 $page_size = 9 if $page_size !~ /^\d+$/;
1474 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1475 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1477 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1478 warn "something's wrong...";
1479 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1483 $string = decode_utf8($string);
1484 $string =~ s/\+/ /go;
1487 my $tree = $supercat->request(
1488 "open-ils.supercat.$axis.browse",
1490 (($axis =~ /^authority/) ? () : ($site)),
1497 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1499 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1500 print $header.$content;
1501 return Apache2::Const::OK;
1504 sub string_startwith {
1506 return Apache2::Const::DECLINED if (-e $apache->filename);
1509 my $year = (gmtime())[5] + 1900;
1511 my $host = $cgi->virtual_host || $cgi->server_name;
1514 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1515 my $rel_name = $cgi->url(-relative=>1);
1516 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1519 my $url = $cgi->url(-path_info=>$add_path);
1520 my $root = (split 'startwith', $url)[0];
1521 my $base = (split 'startwith', $url)[0] . 'startwith';
1522 my $unapi = (split 'startwith', $url)[0] . 'unapi';
1524 my $path = $cgi->path_info;
1527 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1528 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1530 my $status = [$cgi->param('status')];
1531 my $cpLoc = [$cgi->param('copyLocation')];
1532 $site ||= $cgi->param('searchOrg');
1533 $page ||= $cgi->param('startPage') || 0;
1534 $page_size ||= $cgi->param('count') || 9;
1536 $page = 0 if ($page !~ /^-?\d+$/);
1537 $page_size = 9 if $page_size !~ /^\d+$/;
1539 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1540 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1542 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1543 warn "something's wrong...";
1544 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1548 $string = decode_utf8($string);
1549 $string =~ s/\+/ /go;
1552 my $tree = $supercat->request(
1553 "open-ils.supercat.$axis.startwith",
1555 (($axis =~ /^authority/) ? () : ($site)),
1562 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1564 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1565 print $header.$content;
1566 return Apache2::Const::OK;
1569 sub item_age_browse {
1571 return Apache2::Const::DECLINED if (-e $apache->filename);
1574 my $year = (gmtime())[5] + 1900;
1576 my $host = $cgi->virtual_host || $cgi->server_name;
1579 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1580 my $rel_name = $cgi->url(-relative=>1);
1581 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1584 my $url = $cgi->url(-path_info=>$add_path);
1585 my $root = (split 'browse', $url)[0];
1586 my $base = (split 'browse', $url)[0] . 'browse';
1587 my $unapi = (split 'browse', $url)[0] . 'unapi';
1589 my $path = $cgi->path_info;
1592 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1593 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1595 unless ($axis eq 'item-age') {
1596 warn "something's wrong...";
1597 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1601 my $status = [$cgi->param('status')];
1602 my $cpLoc = [$cgi->param('copyLocation')];
1603 $site ||= $cgi->param('searchOrg') || '-';
1604 $page ||= $cgi->param('startPage') || 1;
1605 $page_size ||= $cgi->param('count') || 10;
1607 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1608 $page_size = 10 if $page_size !~ /^\d+$/;
1610 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1611 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1613 my $recs = $supercat->request(
1614 "open-ils.supercat.new_book_list",
1622 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1624 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1625 print $header.$content;
1626 return Apache2::Const::OK;
1629 our %qualifier_ids = (
1630 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1631 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1632 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1636 our %nested_qualifier_map = (
1638 site => ['site','Evergreen Site Code (shortname)'],
1639 sort => ['sort','Sort on relevance, title, author, pubdate, create_date or edit_date'],
1640 direction => ['dir','Sort direction (asc|desc)'],
1641 available => ['available','Filter to available (true|false)'],
1643 author => ['author'],
1645 subject => ['subject'],
1646 keyword => ['keyword'],
1647 series => ['series'],
1651 creator => ['author'],
1652 contributor => ['author'],
1653 publisher => ['keyword'],
1654 subject => ['subject'],
1655 identifier => ['keyword'],
1658 language => ['lang'],
1662 titleAbbreviated => ['title'],
1663 titleUniform => ['title'],
1664 titleTranslated => ['title'],
1665 titleAlternative => ['title'],
1666 titleSeries => ['series'],
1668 # Author/Name class:
1670 namePersonal => ['author'],
1671 namePersonalFamily => ['author'],
1672 namePersonalGiven => ['author'],
1673 nameCorporate => ['author'],
1674 nameConference => ['author'],
1677 subjectPlace => ['subject'],
1678 subjectTitle => ['keyword'],
1679 subjectName => ['subject|name'],
1680 subjectOccupation => ['keyword'],
1685 dateIssued => [undef],
1686 dateCreated => [undef],
1687 dateValid => [undef],
1688 dateModified => [undef],
1689 dateCopyright => [undef],
1692 genre => ['keyword'],
1695 audience => [undef],
1698 originPlace => [undef],
1701 edition => ['keyword'],
1704 volume => ['keyword'],
1705 issue => ['keyword'],
1706 startPage => ['keyword'],
1707 endPage => ['keyword'],
1710 issuance => ['keyword'],
1713 serverChoice => ['keyword'],
1717 # Our authority search options are currently pretty impoverished;
1718 # just right-truncated string match on a few categories, or by
1720 our %nested_auth_qualifier_map = (
1722 id => ['id', 'Record number'],
1723 name => ['author', 'Personal or corporate author, or meeting name'],
1724 title => ['title', 'Uniform title'],
1725 subject => ['subject', 'Chronological term, topical term, geographic name, or genre/form term'],
1726 topic => ['topic', 'Topical term'],
1730 my $base_explain = <<XML;
1732 id="evergreen-sru-explain-full"
1733 authoritative="true"
1734 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1735 xmlns="http://explain.z3950.org/dtd/2.0/">
1736 <serverInfo transport="http" protocol="SRU" version="1.1">
1743 <title primary="true"/>
1744 <description primary="true"/>
1748 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1753 identifier="info:srw/schema/1/marcxml-v1.1"
1754 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1758 <title>MARC21Slim (marcxml)</title>
1763 <default type="numberOfRecords">10</default>
1764 <default type="contextSet">eg</default>
1765 <default type="index">keyword</default>
1766 <default type="relation">all</default>
1767 <default type="sortSchema">marcxml</default>
1768 <default type="retrieveSchema">marcxml</default>
1769 <setting type="maximumRecords">50</setting>
1770 <supports type="relationModifier">relevant</supports>
1771 <supports type="relationModifier">stem</supports>
1772 <supports type="relationModifier">fuzzy</supports>
1773 <supports type="relationModifier">word</supports>
1784 my $req = SRU::Request->newFromCGI( $cgi );
1785 my $resp = SRU::Response->newFromRequest( $req );
1787 # Find the org_unit shortname, if passed as part of the URL
1788 # http://example.com/opac/extras/sru/SHORTNAME
1789 my $url = $cgi->path_info;
1790 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1792 if ( $resp->type eq 'searchRetrieve' ) {
1794 # Older versions of Debian packages returned terms to us double-encoded,
1795 # so we had to forcefully double-decode them a second time with
1796 # an outer decode('utf8', $string) call; this seems to be resolved with
1797 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1798 my $cql_query = decode_utf8($req->query);
1799 my $search_string = decode_utf8($req->cql->toEvergreen);
1801 # Ensure the search string overrides the default site
1802 if ($shortname and $search_string !~ m#site:#) {
1803 $search_string .= " site:$shortname";
1806 my $offset = $req->startRecord;
1807 $offset-- if ($offset);
1810 my $limit = $req->maximumRecords;
1813 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1815 my $recs = $search->request(
1816 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1819 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1821 foreach my $record (@$bre) {
1822 my $marcxml = $record->marc;
1823 # Make the beast conform to a VDX-supported format
1824 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1825 # Trying to implement LIBSOL_852_A format; so much for standards
1827 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1828 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1830 # Force record leader to 'a' as our data is always UTF8
1831 # Avoids marc8_to_utf8 from being invoked with horrible results
1832 # on the off-chance the record leader isn't correct
1833 my $ldr = $marc->leader;
1834 substr($ldr, 9, 1, 'a');
1835 $marc->leader($ldr);
1837 # Expects the record ID in the 001
1838 $marc->delete_field($_) for ($marc->field('001'));
1839 if (!$marc->field('001')) {
1840 $marc->insert_fields_ordered(
1841 MARC::Field->new( '001', $record->id )
1844 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1845 foreach my $cn (keys %$bib_holdings) {
1846 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1847 $marc->insert_fields_ordered(
1850 a => $cp->{'location'},
1851 b => $bib_holdings->{$cn}->{'owning_lib'},
1853 d => $cp->{'circlib'},
1854 g => $cp->{'barcode'},
1855 n => $cp->{'status'},
1861 # Ensure the data is encoded as UTF8 before we hand it off
1862 $marcxml = encode_utf8($marc->as_xml_record());
1863 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1867 SRU::Response::Record->new(
1868 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1869 recordData => $marcxml,
1870 recordPosition => ++$offset
1875 $resp->numberOfRecords($recs->{count});
1877 } elsif ( $resp->type eq 'explain' ) {
1878 return_sru_explain($cgi, $req, $resp, \$ex_doc,
1880 \%OpenILS::WWW::SuperCat::qualifier_ids
1884 SRU::Response::Record->new(
1885 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1886 recordData => $ex_doc
1891 print $cgi->header( -type => 'application/xml' );
1892 print $U->entityize($resp->asXML) . "\n";
1893 return Apache2::Const::OK;
1898 package CQL::BooleanNode;
1902 my $left = $self->left();
1903 my $right = $self->right();
1904 my $leftStr = $left->toEvergreen;
1905 my $rightStr = $right->toEvergreen();
1907 my $op = '||' if uc $self->op() eq 'OR';
1910 return "$leftStr $rightStr";
1913 sub toEvergreenAuth {
1914 return toEvergreen(shift);
1917 package CQL::TermNode;
1921 my $qualifier = $self->getQualifier();
1922 my $term = $self->getTerm();
1923 my $relation = $self->getRelation();
1927 my ($qset, $qname) = split(/\./, $qualifier);
1929 $log->debug("SRU toEvergreen: $qset, $qname $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0]\n");
1931 if ( exists($OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}) ) {
1932 $qualifier = $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0] || 'kw';
1935 my @modifiers = $relation->getModifiers();
1937 my $base = $relation->getBase();
1938 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
1941 foreach my $m ( @modifiers ) {
1942 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
1948 $quote_it = 0 if ( $base eq 'all' );
1949 $term = maybeQuote($term) if $quote_it;
1952 croak( "Evergreen doesn't support the $base relations" );
1960 return "$qualifier:$term";
1963 sub toEvergreenAuth {
1965 my $qualifier = $self->getQualifier();
1966 my $term = $self->getTerm();
1967 my $relation = $self->getRelation();
1971 my ($qset, $qname) = split(/\./, $qualifier);
1973 $log->debug("SRU toEvergreenAuth: $qset, $qname $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}[0]\n");
1975 if ( exists($OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}) ) {
1976 $qualifier = $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}[0] || 'author';
1979 return { qualifier => $qualifier, term => $term };
1984 sub sru_auth_search {
1987 my $req = SRU::Request->newFromCGI( $cgi );
1988 my $resp = SRU::Response->newFromRequest( $req );
1990 if ( $resp->type eq 'searchRetrieve' ) {
1991 return_auth_response($cgi, $req, $resp);
1992 } elsif ( $resp->type eq 'explain' ) {
1993 return_sru_explain($cgi, $req, $resp, \$auth_ex_doc,
1994 \%OpenILS::WWW::SuperCat::nested_auth_qualifier_map,
1995 \%OpenILS::WWW::SuperCat::qualifier_ids
1999 print $cgi->header( -type => 'application/xml' );
2000 print $U->entityize($resp->asXML) . "\n";
2001 return Apache2::Const::OK;
2004 sub explain_header {
2007 my $host = $cgi->virtual_host || $cgi->server_name;
2010 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
2011 my $rel_name = $cgi->url(-relative=>1);
2012 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
2014 my $base = $cgi->url(-base=>1);
2015 my $url = $cgi->url(-path_info=>$add_path);
2016 $url =~ s/^$base\///o;
2018 my $doc = $parser->parse_string($base_explain);
2019 my $e = $doc->documentElement;
2020 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
2021 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
2022 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
2027 sub return_sru_explain {
2028 my ($cgi, $req, $resp, $explain, $index_map, $qualifier_ids) = @_;
2030 $index_map ||= \%qualifier_map;
2032 my ($doc, $e) = explain_header($cgi);
2033 for my $name ( keys %{$index_map} ) {
2035 my $identifier = $qualifier_ids->{ $name };
2037 next unless $identifier;
2039 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
2040 $set_node->setAttribute( identifier => $identifier );
2041 $set_node->setAttribute( name => $name );
2043 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
2044 my %attribute_desc = (
2045 site => 'Evergreen Site Code (shortname)',
2046 sort => 'Sort on relevance, title, author, pubdate, create_date or edit_date',
2047 dir => 'Sort direction (asc|desc)',
2048 available => 'Filter to available (true|false)',
2051 for my $index ( @{$index_map->{$name}} ) {
2053 if (exists $attribute_desc{$title}) {
2054 $title = $attribute_desc{$title};
2057 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
2059 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
2060 $map_node->appendChild( $name_node );
2062 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
2064 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
2065 $index_node->appendChild( $title_node );
2066 $index_node->appendChild( $map_node );
2068 $index_node->setAttribute( id => "$name.$index" );
2069 $title_node->appendText($title);
2070 $name_node->setAttribute( set => $name );
2071 $name_node->appendText($index);
2073 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
2077 $$explain = $e->toString;
2081 SRU::Response::Record->new(
2082 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
2083 recordData => $$explain
2089 sub return_auth_response {
2090 my ($cgi, $req, $resp) = @_;
2092 my $cql_query = decode_utf8($req->query);
2093 my $search = $req->cql->toEvergreenAuth;
2095 my $qualifier = decode_utf8($search->{qualifier});
2096 my $term = decode_utf8($search->{term});
2098 $log->info("SRU NAF search string [$cql_query] converted to "
2099 . "[$qualifier:$term]\n");
2101 my $page_size = $req->maximumRecords;
2104 # startwith deals with pages, so convert startRecord to a page number
2105 my $page = ($req->startRecord / $page_size) || 0;
2108 if ($qualifier eq "id") {
2109 $recs = [ int($term) ];
2111 $recs = $supercat->request(
2112 "open-ils.supercat.authority.$qualifier.startwith", $term, $page_size, $page
2116 my $record_position = $req->startRecord;
2117 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2118 foreach my $record (@$recs) {
2119 my $marcxml = $cstore->request(
2120 'open-ils.cstore.direct.authority.record_entry.retrieve', $record
2124 SRU::Response::Record->new(
2125 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
2126 recordData => $marcxml,
2127 recordPosition => ++$record_position
2132 $resp->numberOfRecords(scalar(@$recs));
2135 =head2 get_ou($org_unit)
2137 Returns an aou object for a given actor.org_unit shortname or ID.
2142 my $org = shift || '-';
2146 $org_unit = $actor->request(
2147 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2149 } elsif ($org !~ /^\d+$/o) {
2150 $org_unit = $actor->request(
2151 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2154 $org_unit = $actor->request(
2155 'open-ils.actor.org_unit_list.search' => id => $org