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;
31 use MARC::File::XML ( BinaryEncoding => 'UTF-8' );
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 my $authority_axis_re = qr/^authority\.(\w+)(\.refs)?$/;
41 $browse_types{call_number}{xml} = sub {
44 my $year = (gmtime())[5] + 1900;
47 $content .= "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>\n";
50 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
51 $cn_class =~ s/Fieldmapper-//gso;
53 my $cn_tag = "tag:open-ils.org,$year:$cn_class/".$cn->id;
54 my $cn_lib = $cn->owning_lib->shortname;
55 my $cn_label = $cn->label;
56 my $cn_prefix = $cn->prefix->label;
57 my $cn_suffix = $cn->suffix->label;
59 $cn_label =~ s/\n//gos;
60 $cn_label =~ s/&/&/go;
61 $cn_label =~ s/'/'/go;
62 $cn_label =~ s/</</go;
63 $cn_label =~ s/>/>/go;
65 $cn_prefix =~ s/\n//gos;
66 $cn_prefix =~ s/&/&/go;
67 $cn_prefix =~ s/'/'/go;
68 $cn_prefix =~ s/</</go;
69 $cn_prefix =~ s/>/>/go;
71 $cn_suffix =~ s/\n//gos;
72 $cn_suffix =~ s/&/&/go;
73 $cn_suffix =~ s/'/'/go;
74 $cn_suffix =~ s/</</go;
75 $cn_suffix =~ s/>/>/go;
77 (my $ou_class = $cn->owning_lib->class_name) =~ s/::/-/gso;
78 $ou_class =~ s/Fieldmapper-//gso;
80 my $ou_tag = "tag:open-ils.org,$year:$ou_class/".$cn->owning_lib->id;
81 my $ou_name = $cn->owning_lib->name;
83 $ou_name =~ s/\n//gos;
84 $ou_name =~ s/'/'/go;
86 (my $rec_class = $cn->record->class_name) =~ s/::/-/gso;
87 $rec_class =~ s/Fieldmapper-//gso;
89 my $rec_tag = "tag:open-ils.org,$year:$rec_class/".$cn->record->id.'/'.$cn->owning_lib->shortname;
91 $content .= "<volume id='$cn_tag' lib='$cn_lib' prefix='$cn_prefix' label='$cn_label' suffix='$cn_suffix'>\n";
92 $content .= "<owning_lib xmlns='http://open-ils.org/spec/actors/v1' id='$ou_tag' name='$ou_name'/>\n";
94 my $r_doc = $parser->parse_string($cn->record->marc);
95 $r_doc->documentElement->setAttribute( id => $rec_tag );
96 $content .= $U->entityize($r_doc->documentElement->toString);
98 $content .= "</volume>\n";
101 $content .= "</volumes>\n";
102 return ("Content-type: application/xml\n\n",$content);
106 $browse_types{call_number}{html} = sub {
111 if (!$cn_browse_xslt) {
112 $cn_browse_xslt = $parser->parse_file(
113 OpenSRF::Utils::SettingsClient
115 ->config_value( dirs => 'xsl' ).
118 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
121 my (undef,$xml) = $browse_types{call_number}{xml}->($tree);
124 "Content-type: text/html\n\n",
126 $cn_browse_xslt->transform(
127 $parser->parse_string( $xml ),
142 OpenSRF::System->bootstrap_client( config_file => $bootstrap );
144 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
145 Fieldmapper->import(IDL => $idl);
147 $supercat = OpenSRF::AppSession->create('open-ils.supercat');
148 $actor = OpenSRF::AppSession->create('open-ils.actor');
149 $search = OpenSRF::AppSession->create('open-ils.search');
150 $parser = new XML::LibXML;
151 $xslt = new XML::LibXSLT;
153 $cn_browse_xslt = $parser->parse_file(
154 OpenSRF::Utils::SettingsClient
156 ->config_value( dirs => 'xsl' ).
160 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
162 %qualifier_map = %{$supercat
163 ->request("open-ils.supercat.biblio.search_aliases")
166 my %attribute_desc = (
167 site => 'Evergreen Site Code (shortname)',
168 sort => 'Sort on relevance, title, author, pubdate, create_date or edit_date',
169 dir => 'Sort direction (asc|desc)',
170 available => 'Filter to available (true|false)',
173 # Append the non-search-alias attributes to the qualifier map
190 preferred_language_weight
191 preferred_language_multiplier
193 $qualifier_map{'eg'}{$_}{'index'} = $_;
194 if (exists $attribute_desc{$_}) {
195 $qualifier_map{'eg'}{$_}{'title'} = $attribute_desc{$_};
197 $qualifier_map{'eg'}{$_}{'title'} = $_;
202 ->request("open-ils.supercat.record.formats")
205 $list = [ map { (keys %$_)[0] } @$list ];
206 push @$list, 'htmlholdings','html', 'marctxt', 'ris';
208 for my $browse_axis ( qw/title author subject topic series item-age/ ) {
209 for my $record_browse_format ( @$list ) {
211 my $__f = $record_browse_format;
212 my $__a = $browse_axis;
214 $browse_types{$__a}{$__f} = sub {
215 my $record_list = shift;
218 my $real_format = shift || $__f;
223 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
224 my $feed = create_record_feed( 'record', $real_format, $record_list, $unapi, $site, undef, $real_format =~ /(-full|-uris)$/o ? 1 : 0 );
225 $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",
239 my $auth_axes = $supercat
240 ->request("open-ils.supercat.authority.browse_axis_list")
244 for my $axis ( @$auth_axes ) {
245 my $basic_axis = 'authority.' . $axis;
246 for my $browse_axis ( ($basic_axis, $basic_axis . ".refs") ) {
249 my $__a = $browse_axis;
251 $browse_types{$__a}{$__f} = sub {
252 my $record_list = shift;
255 my $real_format = shift || $__f;
260 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
261 my $feed = create_record_feed( 'authority', $real_format, $record_list, $unapi, $site, undef, $real_format =~ /-full$/o ? -1 : 0 );
262 $feed->root( "$base/../" );
263 $feed->link( next => $next => $feed->type );
264 $feed->link( previous => $prev => $feed->type );
267 "Content-type: ". $feed->type ."; charset=utf-8\n\n",
276 =head2 parse_feed_type($type)
278 Determines whether and how a given feed type needs to be "fleshed out"
279 with holdings information.
281 The feed type could end with the string "-full", in which case we want
282 to return call numbers, copies, and URIS.
284 Or the feed type could end with "-uris", in which case we want to return
285 call numbers and URIS.
287 Otherwise, we won't return any holdings.
291 sub parse_feed_type {
292 my $type = shift || '';
294 if ($type =~ /-full$/o) {
298 if ($type =~ /-uris$/o) {
302 # Otherwise, we'll return just the facts, ma'am
306 =head2 supercat_format($format_hashref, $format_type)
308 Given a reference to a hash containing the namespace_uri,
309 docs, and schema location attributes for a set of formats,
310 generate the XML description required by the supercat service.
312 We derive the base type from the format type so that we do not
313 have to populate the hash with redundant information.
317 sub supercat_format {
321 (my $base_type = $type) =~ s/(-full|-uris)$//o;
323 my $format = "<format><name>$type</name><type>application/xml</type>";
325 for my $part ( qw/namespace_uri docs schema_location/ ) {
326 $format .= "<$part>$$h{$base_type}{$part}</$part>"
327 if ($$h{$base_type}{$part});
330 $format .= '</format>';
335 =head2 unapi_format($format_hashref, $format_type)
337 Given a reference to a hash containing the namespace_uri,
338 docs, and schema location attributes for a set of formats,
339 generate the XML description required by the supercat service.
341 We derive the base type from the format type so that we do not
342 have to populate the hash with redundant information.
350 (my $base_type = $type) =~ s/(-full|-uris)$//o;
352 my $format = "<format name='$type' type='application/xml'";
354 for my $part ( qw/namespace_uri docs schema_location/ ) {
355 $format .= " $part='$$h{$base_type}{$part}'"
356 if ($$h{$base_type}{$part});
368 return Apache2::Const::DECLINED if (-e $apache->filename);
370 (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
373 ->request("open-ils.supercat.oisbn", $isbn)
376 print "Content-type: application/xml; charset=utf-8\n\n";
377 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
379 unless (exists $$list{metarecord}) {
381 return Apache2::Const::OK;
384 print "<idlist metarecord='$$list{metarecord}'>\n";
386 for ( keys %{ $$list{record_list} } ) {
387 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
388 print " <isbn record='$_'>$o</isbn>\n"
393 return Apache2::Const::OK;
399 return Apache2::Const::DECLINED if (-e $apache->filename);
404 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
405 my $rel_name = $cgi->url(-relative=>1);
406 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
409 my $url = $cgi->url(-path_info=>$add_path);
410 my $root = (split 'unapi', $url)[0];
411 my $base = (split 'unapi', $url)[0] . 'unapi';
414 my $uri = $cgi->param('id') || '';
415 my $host = $cgi->virtual_host || $cgi->server_name;
417 my $skin = $cgi->param('skin') || 'default';
418 my $locale = $cgi->param('locale') || 'en-US';
420 # Enable localized results of copy status, etc
421 $supercat->session_locale($locale);
423 my $format = $cgi->param('format') || '';
424 my $flesh_feed = parse_feed_type($format);
425 (my $base_format = $format) =~ s/(-full|-uris)$//o;
426 my ($id,$type,$command,$lib,$depth,$paging) = ('','record','');
427 my $body = "Content-type: application/xml; charset=utf-8\n\n";
429 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
432 ($lib,$depth) = split('/', $4);
433 $type = 'metarecord' if ($1 =~ /^m/o);
434 $type = 'authority' if ($1 =~ /^authority/o);
438 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
441 ->request("open-ils.supercat.$type.formats")
444 if ($type eq 'record' or $type eq 'isbn') {
445 $body .= <<" FORMATS";
447 <format name='opac' type='text/html'/>
448 <format name='html' type='text/html'/>
449 <format name='htmlholdings' type='text/html'/>
450 <format name='holdings_xml' type='application/xml'/>
451 <format name='holdings_xml-full' type='application/xml'/>
452 <format name='html-full' type='text/html'/>
453 <format name='htmlholdings-full' type='text/html'/>
454 <format name='marctxt' type='text/plain'/>
455 <format name='ris' type='text/plain'/>
457 } elsif ($type eq 'metarecord') {
458 $body .= <<" FORMATS";
460 <format name='opac' type='text/html'/>
463 $body .= <<" FORMATS";
469 my ($type) = keys %$h;
470 $body .= unapi_format($h, $type);
472 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
473 $body .= unapi_format($h, "$type-full");
474 $body .= unapi_format($h, "$type-uris");
478 $body .= "</formats>\n";
482 ->request("open-ils.supercat.$type.formats")
487 ->request("open-ils.supercat.metarecord.formats")
491 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
492 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
494 $body .= <<" FORMATS";
496 <format name='opac' type='text/html'/>
497 <format name='html' type='text/html'/>
498 <format name='htmlholdings' type='text/html'/>
499 <format name='holdings_xml' type='application/xml'/>
500 <format name='holdings_xml-full' type='application/xml'/>
501 <format name='html-full' type='text/html'/>
502 <format name='htmlholdings-full' type='text/html'/>
503 <format name='marctxt' type='text/plain'/>
504 <format name='ris' type='text/plain'/>
509 my ($type) = keys %$h;
510 $body .= "\t" . unapi_format($h, $type);
512 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
513 $body .= "\t" . unapi_format($h, "$type-full");
514 $body .= "\t" . unapi_format($h, "$type-uris");
518 $body .= "</formats>\n";
522 return Apache2::Const::OK;
526 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
530 ($lib,$depth) = split('/', $4);
532 $type = 'metarecord' if ($scheme =~ /^metabib/o);
533 $type = 'isbn' if ($scheme =~ /^isbn/o);
534 $type = 'acp' if ($scheme =~ /^asset-copy/o);
535 $type = 'acn' if ($scheme =~ /^asset-call_number/o);
536 $type = 'auri' if ($scheme =~ /^asset-uri/o);
537 $type = 'authority' if ($scheme =~ /^authority/o);
538 $command = 'retrieve';
539 $command = 'browse' if (grep { $scheme eq $_ } qw/call_number title author subject topic authority.title authority.author authority.subject authority.topic series item-age/);
540 $command = 'browse' if ($scheme =~ /^authority/);
544 $paging = [split ',', $paging];
549 if (!$lib || $lib eq '-') {
550 $lib = $actor->request(
551 'open-ils.actor.org_unit_list.search' => parent_ou => undef
552 )->gather(1)->[0]->shortname;
555 my ($lib_object,$lib_id,$ou_types,$lib_depth);
556 if ($type ne 'acn' && $type ne 'acp' && $type ne 'auri') {
557 $lib_object = $actor->request(
558 'open-ils.actor.org_unit_list.search' => shortname => $lib
560 $lib_id = $lib_object->id;
562 $ou_types = $actor->request( 'open-ils.actor.org_types.retrieve' )->gather(1);
563 $lib_depth = defined($depth) ? $depth : (grep { $_->id == $lib_object->ou_type } @$ou_types)[0]->depth;
566 if ($command eq 'browse') {
567 print "Location: $root/browse/$base_format/$scheme/$lib/$id\n\n";
571 if ($type eq 'isbn') {
572 my $rec = $supercat->request('open-ils.supercat.isbn.object.retrieve',$id)->gather(1);
574 # Escape user input before display
575 $command = CGI::escapeHTML($command);
576 $id = CGI::escapeHTML($id);
577 $type = CGI::escapeHTML($type);
578 $format = CGI::escapeHTML(decode_utf8($format));
580 print "Content-type: text/html; charset=utf-8\n\n";
581 $apache->custom_response( 404, <<" HTML");
584 <title>Type [$type] with id [$id] not found!</title>
588 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
599 { (keys(%$_))[0] eq $base_format }
600 @{ $supercat->request("open-ils.supercat.$type.formats")->gather(1) }
602 { $_ eq $base_format }
603 qw/opac html htmlholdings marctxt ris holdings_xml/
605 # Escape user input before display
606 $format = CGI::escapeHTML($format);
607 $type = CGI::escapeHTML($type);
609 print "Content-type: text/html; charset=utf-8\n\n";
610 $apache->custom_response( 406, <<" HTML");
613 <title>Invalid format [$format] for type [$type]!</title>
617 <center>Sorry, format $format is not valid for type $type.</center>
624 if ($format eq 'opac') {
625 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id&l=$lib_id&d=$lib_depth\n\n"
626 if ($type eq 'metarecord');
627 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id&l=$lib_id&d=$lib_depth\n\n"
628 if ($type eq 'record');
630 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format) && ($type ne 'acn' && $type ne 'acp' && $type ne 'auri')) {
631 my $feed = create_record_feed(
642 # Escape user input before display
643 $command = CGI::escapeHTML($command);
644 $id = CGI::escapeHTML($id);
645 $type = CGI::escapeHTML($type);
646 $format = CGI::escapeHTML(decode_utf8($format));
648 print "Content-type: text/html; charset=utf-8\n\n";
649 $apache->custom_response( 404, <<" HTML");
652 <title>Type [$type] with id [$id] not found!</title>
656 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
664 $feed->creator($host);
666 $feed->link( unapi => $base) if ($flesh_feed);
668 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
669 print $U->entityize($feed->toString) . "\n";
671 return Apache2::Const::OK;
674 my $method = "open-ils.supercat.$type.$base_format.$command";
676 push @params, $lib, $lib_depth, $flesh_feed, $paging if ($base_format eq 'holdings_xml');
678 # for acn, acp, etc, the "lib" pathinfo position isn't useful.
679 # however, we can have it carry extra options like no_record! (comma separated)
680 push @params, { map { ( $_ => 1 ) } split(',', $lib) } if ( grep { $type eq $_} qw/acn acp auri/);
682 my $req = $supercat->request($method,@params);
683 my $data = $req->gather();
685 if ($req->failed || !$data) {
686 # Escape user input before display
687 $command = CGI::escapeHTML($command);
688 $id = CGI::escapeHTML($id);
689 $type = CGI::escapeHTML($type);
690 $format = CGI::escapeHTML(decode_utf8($format));
692 print "Content-type: text/html; charset=utf-8\n\n";
693 $apache->custom_response( 404, <<" HTML");
696 <title>$type $id not found!</title>
700 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
707 print "Content-type: application/xml; charset=utf-8\n\n";
709 # holdings_xml format comes back to us without an XML declaration
710 # and without being entityized; fix that here
711 if ($base_format eq 'holdings_xml') {
712 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
713 print $U->entityize($data);
715 while (my $c = $req->recv) {
716 print $U->entityize($c->content);
722 return Apache2::Const::OK;
728 return Apache2::Const::DECLINED if (-e $apache->filename);
733 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
734 my $rel_name = $cgi->url(-relative=>1);
735 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
738 my $url = $cgi->url(-path_info=>$add_path);
739 my $root = (split 'supercat', $url)[0];
740 my $base = (split 'supercat', $url)[0] . 'supercat';
741 my $unapi = (split 'supercat', $url)[0] . 'unapi';
743 my $host = $cgi->virtual_host || $cgi->server_name;
745 my $path = $cgi->path_info;
746 my ($id,$type,$format,$command) = reverse split '/', $path;
747 my $flesh_feed = parse_feed_type($format);
748 (my $base_format = $format) =~ s/(-full|-uris)$//o;
750 my $skin = $cgi->param('skin') || 'default';
751 my $locale = $cgi->param('locale') || 'en-US';
753 # Enable localized results of copy status, etc
754 $supercat->session_locale($locale);
756 if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
757 print "Content-type: application/xml; charset=utf-8\n";
760 ->request("open-ils.supercat.$1.formats")
768 <type>text/html</type>
771 if ($1 eq 'record' or $1 eq 'isbn') {
773 <name>htmlholdings</name>
774 <type>text/html</type>
778 <type>text/html</type>
781 <name>htmlholdings-full</name>
782 <type>text/html</type>
785 <name>html-full</name>
786 <type>text/html</type>
790 <type>text/plain</type>
794 <type>text/plain</type>
799 my ($type) = keys %$h;
800 print supercat_format($h, $type);
802 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
803 print supercat_format($h, "$type-full");
804 print supercat_format($h, "$type-uris");
809 print "</formats>\n";
811 return Apache2::Const::OK;
815 ->request("open-ils.supercat.record.formats")
820 ->request("open-ils.supercat.metarecord.formats")
824 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
825 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
830 <type>text/html</type>
833 <name>htmlholdings</name>
834 <type>text/html</type>
838 <type>text/html</type>
841 <name>htmlholdings-full</name>
842 <type>text/html</type>
845 <name>html-full</name>
846 <type>text/html</type>
850 <type>text/plain</type>
854 <type>text/plain</type>
858 my ($type) = keys %$h;
859 print supercat_format($h, $type);
861 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
862 print supercat_format($h, "$type-full");
863 print supercat_format($h, "$type-uris");
868 print "</formats>\n";
871 return Apache2::Const::OK;
874 if ($format eq 'opac') {
875 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id\n\n"
876 if ($type eq 'metarecord');
877 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id\n\n"
878 if ($type eq 'record');
881 } elsif ($base_format eq 'marc21') {
885 my $bib = $supercat->request( "open-ils.supercat.record.object.retrieve", $id )->gather(1)->[0];
887 print "Content-type: application/octet-stream\n\n" . MARC::Record->new_from_xml( $bib->marc, 'UTF-8', 'USMARC' )->as_usmarc;
892 # Escape user input before display
893 $id = CGI::escapeHTML($id);
895 print "Content-type: text/html; charset=utf-8\n\n";
896 $apache->custom_response( 404, <<" HTML");
903 <center>Couldn't fetch $id as MARC21.</center>
910 return Apache2::Const::OK;
912 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
913 my $feed = create_record_feed(
921 $feed->creator($host);
925 $feed->link( unapi => $base) if ($flesh_feed);
927 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
928 print $U->entityize($feed->toString) . "\n";
930 return Apache2::Const::OK;
933 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
937 # Escape user input before display
938 $command = CGI::escapeHTML($command);
939 $id = CGI::escapeHTML($id);
940 $type = CGI::escapeHTML($type);
941 $format = CGI::escapeHTML(decode_utf8($format));
943 print "Content-type: text/html; charset=utf-8\n\n";
944 $apache->custom_response( 404, <<" HTML");
947 <title>$type $id not found!</title>
951 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
958 print "Content-type: application/xml; charset=utf-8\n\n";
959 print $U->entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
961 return Apache2::Const::OK;
967 return Apache2::Const::DECLINED if (-e $apache->filename);
971 my $year = (gmtime())[5] + 1900;
972 my $host = $cgi->virtual_host || $cgi->server_name;
975 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
976 my $rel_name = $cgi->url(-relative=>1);
977 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
980 my $url = $cgi->url(-path_info=>$add_path);
981 my $root = (split 'feed', $url)[0] . '/';
982 my $base = (split 'bookbag', $url)[0] . '/bookbag';
983 my $unapi = (split 'feed', $url)[0] . '/unapi';
985 my $skin = $cgi->param('skin') || 'default';
986 my $locale = $cgi->param('locale') || 'en-US';
987 my $org = $cgi->param('searchOrg');
989 # Enable localized results of copy status, etc
990 $supercat->session_locale($locale);
992 my $org_unit = get_ou($org);
993 my $scope = "l=" . $org_unit->[0]->id . "&";
995 $root =~ s{(?<!http:)//}{/}go;
996 $base =~ s{(?<!http:)//}{/}go;
997 $unapi =~ s{(?<!http:)//}{/}go;
999 my $path = $cgi->path_info;
1000 #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
1002 my ($id,$type) = reverse split '/', $path;
1003 my $flesh_feed = parse_feed_type($type);
1005 my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
1006 return Apache2::Const::NOT_FOUND unless($bucket);
1008 my $bucket_tag = "tag:$host,$year:record_bucket/$id";
1009 if ($type eq 'opac') {
1010 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1011 join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
1016 my $feed = create_record_feed(
1019 [ map { $_->target_biblio_record_entry } @{ $bucket->items } ],
1021 $org_unit->[0]->shortname,
1026 $feed->id($bucket_tag);
1028 $feed->title("Items in Book Bag [".$bucket->name."]");
1029 $feed->description($bucket->description || ("Items in Book Bag [".$bucket->name."]"));
1030 $feed->creator($host);
1033 $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
1034 $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
1035 $feed->link(html => $base . "/html-full/$id" => 'text/html');
1036 $feed->link(unapi => $unapi);
1040 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1041 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
1046 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1047 print $U->entityize($feed->toString) . "\n";
1049 return Apache2::Const::OK;
1054 return Apache2::Const::DECLINED if (-e $apache->filename);
1058 my $year = (gmtime())[5] + 1900;
1059 my $host = $cgi->virtual_host || $cgi->server_name;
1062 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1063 my $rel_name = $cgi->url(-relative=>1);
1064 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1067 my $url = $cgi->url(-path_info=>$add_path);
1068 my $root = (split 'feed', $url)[0];
1069 my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
1070 my $unapi = (split 'feed', $url)[0] . 'unapi';
1072 my $skin = $cgi->param('skin') || 'default';
1073 my $locale = $cgi->param('locale') || 'en-US';
1074 my $org = $cgi->param('searchOrg');
1076 # Enable localized results of copy status, etc
1077 $supercat->session_locale($locale);
1079 my $org_unit = get_ou($org);
1080 my $scope = "l=" . $org_unit->[0]->id . "&";
1082 my $path = $cgi->path_info;
1083 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1085 $path =~ s/^\/(?:feed\/)?freshmeat\///og;
1087 my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
1088 my $flesh_feed = parse_feed_type($type);
1091 $limit = 10 if $limit !~ /^\d+$/;
1093 my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
1095 #if ($type eq 'opac') {
1096 # print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
1097 # join('&', map { "rl=" . $_ } @$list) .
1102 my $search = 'record';
1103 if ($rtype eq 'authority') {
1104 $search = 'authority';
1106 my $feed = create_record_feed( $search, $type, $list, $unapi, $org_unit->[0]->shortname, undef, $flesh_feed);
1110 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
1112 $feed->title("$limit most recent $rtype ${axis}s");
1115 $feed->creator($host);
1118 $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
1119 $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
1120 $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
1121 $feed->link(unapi => $unapi);
1125 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1126 join('&', map { 'rl=' . $_} @$list ),
1131 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1132 print $U->entityize($feed->toString) . "\n";
1134 return Apache2::Const::OK;
1137 sub opensearch_osd {
1138 my $version = shift;
1143 if ($version eq '1.0') {
1145 Content-type: application/opensearchdescription+xml; charset=utf-8
1147 <?xml version="1.0" encoding="UTF-8"?>
1148 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
1149 <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&startPage={startPage}&startIndex={startIndex}&count={count}</Url>
1150 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
1151 <ShortName>$lib</ShortName>
1152 <LongName>Search $lib</LongName>
1153 <Description>Search the $lib OPAC by $class.</Description>
1154 <Tags>$lib book library</Tags>
1155 <SampleSearch>harry+potter</SampleSearch>
1156 <Developer>Mike Rylander for GPLS/PINES</Developer>
1157 <Contact>feedback\@open-ils.org</Contact>
1158 <SyndicationRight>open</SyndicationRight>
1159 <AdultContent>false</AdultContent>
1160 </OpenSearchDescription>
1164 Content-type: application/opensearchdescription+xml; charset=utf-8
1166 <?xml version="1.0" encoding="UTF-8"?>
1167 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
1168 <ShortName>$lib</ShortName>
1169 <Description>Search the $lib OPAC by $class.</Description>
1170 <Tags>$lib book library</Tags>
1171 <Url type="application/rss+xml"
1172 template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1173 <Url type="application/atom+xml"
1174 template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1175 <Url type="application/x-mods3+xml"
1176 template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1177 <Url type="application/x-mods+xml"
1178 template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1179 <Url type="application/x-marcxml+xml"
1180 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1181 <Url type="text/html"
1182 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1183 <LongName>Search $lib</LongName>
1184 <Query role="example" searchTerms="harry+potter" />
1185 <Developer>Mike Rylander for GPLS/PINES</Developer>
1186 <Contact>feedback\@open-ils.org</Contact>
1187 <SyndicationRight>open</SyndicationRight>
1188 <AdultContent>false</AdultContent>
1189 <Language>en-US</Language>
1190 <OutputEncoding>UTF-8</OutputEncoding>
1191 <InputEncoding>UTF-8</InputEncoding>
1192 </OpenSearchDescription>
1196 return Apache2::Const::OK;
1199 sub opensearch_feed {
1201 return Apache2::Const::DECLINED if (-e $apache->filename);
1204 my $year = (gmtime())[5] + 1900;
1206 my $host = $cgi->virtual_host || $cgi->server_name;
1209 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1210 my $rel_name = $cgi->url(-relative=>1);
1211 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1214 my $url = $cgi->url(-path_info=>$add_path);
1215 my $root = (split 'opensearch', $url)[0];
1216 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1217 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1219 my $path = $cgi->path_info;
1220 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1222 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1228 if (!$lib || $lib eq '-') {
1229 $lib = $actor->request(
1230 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1231 )->gather(1)->[0]->shortname;
1234 if ($class eq '-') {
1238 return opensearch_osd($version, $lib, $class, $base);
1242 my $page = $cgi->param('startPage') || 1;
1243 my $offset = $cgi->param('startIndex') || 1;
1244 my $limit = $cgi->param('count') || 10;
1246 $page = 1 if ($page !~ /^\d+$/);
1247 $offset = 1 if ($offset !~ /^\d+$/);
1248 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1251 $offset = ($page - 1) * $limit;
1256 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1257 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1259 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1260 $lang = '' if ($lang eq '*');
1262 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1264 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1267 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1268 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1270 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1273 $type = $cgi->param('responseType') if $cgi->param('responseType');
1276 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1280 my $kwt = $cgi->param('kw');
1281 my $tit = $cgi->param('ti');
1282 my $aut = $cgi->param('au');
1283 my $sut = $cgi->param('su');
1284 my $set = $cgi->param('se');
1286 $terms .= " " if ($terms && $kwt);
1287 $terms .= "keyword: $kwt" if ($kwt);
1288 $terms .= " " if ($terms && $tit);
1289 $terms .= "title: $tit" if ($tit);
1290 $terms .= " " if ($terms && $aut);
1291 $terms .= "author: $aut" if ($aut);
1292 $terms .= " " if ($terms && $sut);
1293 $terms .= "subject: $sut" if ($sut);
1294 $terms .= " " if ($terms && $set);
1295 $terms .= "series: $set" if ($set);
1297 if ($version eq '1.0') {
1299 } elsif ($type eq '-') {
1302 my $flesh_feed = parse_feed_type($type);
1304 $terms = decode_utf8($terms);
1305 $lang = 'eng' if ($lang eq 'en-US');
1307 $log->debug("OpenSearch terms: $terms");
1309 my $org_unit = get_ou($org);
1311 # Apostrophes break search and get indexed as spaces anyway
1312 my $safe_terms = $terms;
1313 $safe_terms =~ s{'}{ }go;
1315 my $recs = $search->request(
1316 'open-ils.search.biblio.multiclass.query' => {
1317 org_unit => $org_unit->[0]->id,
1321 sort_dir => $sortdir,
1322 default_class => $class,
1323 ($lang ? ( 'language' => $lang ) : ()),
1324 } => $safe_terms => 1
1327 $log->debug("Hits for [$terms]: $recs->{count}");
1329 my $feed = create_record_feed(
1332 [ map { $_->[0] } @{$recs->{ids}} ],
1339 $log->debug("Feed created...");
1343 $feed->search($safe_terms);
1344 $feed->class($class);
1346 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1348 $feed->creator($host);
1351 $feed->_create_node(
1352 $feed->{item_xpath},
1353 'http://a9.com/-/spec/opensearch/1.1/',
1358 $feed->_create_node(
1359 $feed->{item_xpath},
1360 'http://a9.com/-/spec/opensearch/1.1/',
1365 $feed->_create_node(
1366 $feed->{item_xpath},
1367 'http://a9.com/-/spec/opensearch/1.1/',
1372 $log->debug("...basic feed data added...");
1376 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1377 'application/opensearch+xml'
1378 ) if ($offset + $limit < $recs->{count});
1382 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1383 'application/opensearch+xml'
1388 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1389 'application/opensearch+xml'
1394 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1395 'application/rss+xml'
1400 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1401 'application/atom+xml'
1406 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1412 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1416 $feed->link( 'unapi-server' => $unapi);
1418 $log->debug("...feed links added...");
1422 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1423 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1427 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1428 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1430 $log->debug("...and feed returned.");
1432 return Apache2::Const::OK;
1435 sub create_record_feed {
1438 my $records = shift;
1441 my $lib = uc(shift()) || '-';
1448 my $base = $cgi->url;
1449 my $host = $cgi->virtual_host || $cgi->server_name;
1451 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
1455 my $tag_prefix = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d", $month, $day);
1457 my $flesh_feed = defined($flesh) ? $flesh : parse_feed_type($type);
1459 $type =~ s/(-full|-uris)$//o;
1461 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1462 $feed->base($base) if ($flesh);
1463 $feed->unapi($unapi) if ($flesh);
1465 $type = 'atom' if ($type eq 'html');
1466 $type = 'marcxml' if (($type eq 'htmlholdings') || ($type eq 'marctxt') || ($type eq 'ris'));
1468 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1471 for my $record (@$records) {
1472 next unless($record);
1474 #my $rec = $record->id;
1477 my $item_tag = "$tag_prefix:biblio-record_entry/$rec/$lib";
1478 $item_tag = "$tag_prefix:metabib-metarecord/$rec/$lib" if ($search eq 'metarecord');
1479 $item_tag = "$tag_prefix:isbn/$rec/$lib" if ($search eq 'isbn');
1480 $item_tag .= "/$depth" if (defined($depth));
1482 $item_tag = "$tag_prefix:authority-record_entry/$rec" if ($search eq 'authority');
1484 my $xml = $supercat->request(
1485 "open-ils.supercat.$search.$type.retrieve",
1490 my $node = $feed->add_item($xml);
1494 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && ($flesh > 0)) {
1495 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
1496 while ( !$r->complete ) {
1497 $xml .= join('', map {$_->content} $r->recv);
1499 $xml .= join('', map {$_->content} $r->recv);
1500 $node->add_holdings($xml);
1503 $node->id($item_tag);
1504 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1505 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
1506 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
1507 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1508 $node->link('unapi-id' => $item_tag) if ($flesh);
1516 return Apache2::Const::DECLINED if (-e $apache->filename);
1519 my $year = (gmtime())[5] + 1900;
1521 my $host = $cgi->virtual_host || $cgi->server_name;
1524 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1525 my $rel_name = $cgi->url(-relative=>1);
1526 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1529 my $url = $cgi->url(-path_info=>$add_path);
1530 my $root = (split 'browse', $url)[0];
1531 my $base = (split 'browse', $url)[0] . 'browse';
1532 my $unapi = (split 'browse', $url)[0] . 'unapi';
1534 my $path = $cgi->path_info;
1537 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1538 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1540 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1542 my $status = [$cgi->param('status')];
1543 my $cpLoc = [$cgi->param('copyLocation')];
1544 $site ||= $cgi->param('searchOrg');
1545 $page ||= $cgi->param('startPage') || 0;
1546 $page_size ||= $cgi->param('count') || 9;
1548 $page = 0 if ($page !~ /^-?\d+$/);
1549 $page_size = 9 if $page_size !~ /^\d+$/;
1551 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1552 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1554 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1555 warn "something's wrong...";
1556 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1560 $string = decode_utf8($string);
1561 $string =~ s/\+/ /go;
1565 if ($axis =~ /^authority/) {
1566 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1568 my $method = "open-ils.supercat.authority.browse_center.by_axis";
1569 $method .= ".refs" if $refs;
1571 $tree = $supercat->request(
1579 $tree = $supercat->request(
1580 "open-ils.supercat.$axis.browse",
1590 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1592 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1593 print $header.$content;
1594 return Apache2::Const::OK;
1597 sub string_startwith {
1599 return Apache2::Const::DECLINED if (-e $apache->filename);
1602 my $year = (gmtime())[5] + 1900;
1604 my $host = $cgi->virtual_host || $cgi->server_name;
1607 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1608 my $rel_name = $cgi->url(-relative=>1);
1609 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1612 my $url = $cgi->url(-path_info=>$add_path);
1613 my $root = (split 'startwith', $url)[0];
1614 my $base = (split 'startwith', $url)[0] . 'startwith';
1615 my $unapi = (split 'startwith', $url)[0] . 'unapi';
1617 my $path = $cgi->path_info;
1620 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1621 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1623 my $status = [$cgi->param('status')];
1624 my $cpLoc = [$cgi->param('copyLocation')];
1625 $site ||= $cgi->param('searchOrg');
1626 $page ||= $cgi->param('startPage') || 0;
1627 $page_size ||= $cgi->param('count') || 9;
1629 $page = 0 if ($page !~ /^-?\d+$/);
1630 $page_size = 9 if $page_size !~ /^\d+$/;
1632 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1633 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1635 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1636 warn "something's wrong...";
1637 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1641 $string = decode_utf8($string);
1642 $string =~ s/\+/ /go;
1646 if ($axis =~ /^authority/) {
1647 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1649 my $method = "open-ils.supercat.authority.browse_top.by_axis";
1650 $method .= ".refs" if $refs;
1652 $tree = $supercat->request(
1660 $tree = $supercat->request(
1661 "open-ils.supercat.$axis.startwith",
1671 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1673 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1674 print $header.$content;
1675 return Apache2::Const::OK;
1678 sub item_age_browse {
1680 return Apache2::Const::DECLINED if (-e $apache->filename);
1683 my $year = (gmtime())[5] + 1900;
1685 my $host = $cgi->virtual_host || $cgi->server_name;
1688 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1689 my $rel_name = $cgi->url(-relative=>1);
1690 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1693 my $url = $cgi->url(-path_info=>$add_path);
1694 my $root = (split 'browse', $url)[0];
1695 my $base = (split 'browse', $url)[0] . 'browse';
1696 my $unapi = (split 'browse', $url)[0] . 'unapi';
1698 my $path = $cgi->path_info;
1701 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1702 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1704 unless ($axis eq 'item-age') {
1705 warn "something's wrong...";
1706 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1710 my $status = [$cgi->param('status')];
1711 my $cpLoc = [$cgi->param('copyLocation')];
1712 $site ||= $cgi->param('searchOrg') || '-';
1713 $page ||= $cgi->param('startPage') || 1;
1714 $page_size ||= $cgi->param('count') || 10;
1716 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1717 $page_size = 10 if $page_size !~ /^\d+$/;
1719 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1720 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1722 my $recs = $supercat->request(
1723 "open-ils.supercat.new_book_list",
1731 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1733 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1734 print $header.$content;
1735 return Apache2::Const::OK;
1738 our %qualifier_ids = (
1739 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1740 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1741 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1745 # Our authority search options are currently pretty impoverished;
1746 # just right-truncated string match on a few categories, or by
1748 our %nested_auth_qualifier_map = (
1750 id => { index => 'id', title => 'Record number'},
1751 name => { index => 'author', title => 'Personal or corporate author, or meeting name'},
1752 title => { index => 'title', title => 'Uniform title'},
1753 subject => { index => 'subject', title => 'Chronological term, topical term, geographic name, or genre/form term'},
1754 topic => { index => 'topic', title => 'Topical term'},
1758 my $base_explain = <<XML;
1760 id="evergreen-sru-explain-full"
1761 authoritative="true"
1762 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1763 xmlns="http://explain.z3950.org/dtd/2.0/">
1764 <serverInfo transport="http" protocol="SRU" version="1.1">
1771 <title primary="true"/>
1772 <description primary="true"/>
1776 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1781 identifier="info:srw/schema/1/marcxml-v1.1"
1782 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1786 <title>MARC21Slim (marcxml)</title>
1791 <default type="numberOfRecords">10</default>
1792 <default type="contextSet">eg</default>
1793 <default type="index">keyword</default>
1794 <default type="relation">all</default>
1795 <default type="sortSchema">marcxml</default>
1796 <default type="retrieveSchema">marcxml</default>
1797 <setting type="maximumRecords">50</setting>
1798 <supports type="relationModifier">relevant</supports>
1799 <supports type="relationModifier">stem</supports>
1800 <supports type="relationModifier">fuzzy</supports>
1801 <supports type="relationModifier">word</supports>
1812 my $req = SRU::Request->newFromCGI( $cgi );
1813 my $resp = SRU::Response->newFromRequest( $req );
1815 # Find the org_unit shortname, if passed as part of the URL
1816 # http://example.com/opac/extras/sru/SHORTNAME
1817 my $url = $cgi->path_info;
1818 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1820 if ( $resp->type eq 'searchRetrieve' ) {
1822 # Older versions of Debian packages returned terms to us double-encoded,
1823 # so we had to forcefully double-decode them a second time with
1824 # an outer decode('utf8', $string) call; this seems to be resolved with
1825 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1826 my $cql_query = decode_utf8($req->query);
1827 my $search_string = decode_utf8($req->cql->toEvergreen);
1829 # Ensure the search string overrides the default site
1830 if ($shortname and $search_string !~ m#site:#) {
1831 $search_string .= " site:$shortname";
1834 my $offset = $req->startRecord;
1835 $offset-- if ($offset);
1838 my $limit = $req->maximumRecords;
1841 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1843 my $recs = $search->request(
1844 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1847 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1849 foreach my $record (@$bre) {
1850 my $marcxml = $record->marc;
1851 # Make the beast conform to a VDX-supported format
1852 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1853 # Trying to implement LIBSOL_852_A format; so much for standards
1855 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1856 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1858 # Force record leader to 'a' as our data is always UTF8
1859 # Avoids marc8_to_utf8 from being invoked with horrible results
1860 # on the off-chance the record leader isn't correct
1861 my $ldr = $marc->leader;
1862 substr($ldr, 9, 1, 'a');
1863 $marc->leader($ldr);
1865 # Expects the record ID in the 001
1866 $marc->delete_field($_) for ($marc->field('001'));
1867 if (!$marc->field('001')) {
1868 $marc->insert_fields_ordered(
1869 MARC::Field->new( '001', $record->id )
1872 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1873 foreach my $cn (keys %$bib_holdings) {
1874 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1875 $marc->insert_fields_ordered(
1878 a => $cp->{'location'},
1879 b => $bib_holdings->{$cn}->{'owning_lib'},
1881 d => $cp->{'circlib'},
1882 g => $cp->{'barcode'},
1883 n => $cp->{'status'},
1889 # Ensure the data is encoded as UTF8 before we hand it off
1890 $marcxml = encode_utf8($marc->as_xml_record());
1891 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1895 SRU::Response::Record->new(
1896 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1897 recordData => $marcxml,
1898 recordPosition => ++$offset
1903 $resp->numberOfRecords($recs->{count});
1905 } elsif ( $resp->type eq 'explain' ) {
1906 return_sru_explain($cgi, $req, $resp, \$ex_doc,
1908 \%OpenILS::WWW::SuperCat::qualifier_ids
1912 SRU::Response::Record->new(
1913 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1914 recordData => $ex_doc
1919 print $cgi->header( -type => 'application/xml' );
1920 print $U->entityize($resp->asXML) . "\n";
1921 return Apache2::Const::OK;
1926 package CQL::BooleanNode;
1930 my $left = $self->left();
1931 my $right = $self->right();
1932 my $leftStr = $left->toEvergreen;
1933 my $rightStr = $right->toEvergreen();
1935 my $op = '||' if uc $self->op() eq 'OR';
1938 return "$leftStr $rightStr";
1941 sub toEvergreenAuth {
1942 return toEvergreen(shift);
1945 package CQL::TermNode;
1949 my $qualifier = $self->getQualifier();
1950 my $term = $self->getTerm();
1951 my $relation = $self->getRelation();
1955 my ($qset, $qname) = split(/\./, $qualifier);
1957 if ( exists($qualifier_map{$qset}{$qname}) ) {
1958 $qualifier = $qualifier_map{$qset}{$qname}{'index'} || 'kw';
1959 $log->debug("SRU toEvergreen: $qset, $qname $qualifier_map{$qset}{$qname}{'index'}\n");
1962 my @modifiers = $relation->getModifiers();
1964 my $base = $relation->getBase();
1965 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
1968 foreach my $m ( @modifiers ) {
1969 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
1975 $quote_it = 0 if ( $base eq 'all' );
1976 $term = maybeQuote($term) if $quote_it;
1979 croak( "Evergreen doesn't support the $base relations" );
1987 return "$qualifier:$term";
1990 sub toEvergreenAuth {
1992 my $qualifier = $self->getQualifier();
1993 my $term = $self->getTerm();
1994 my $relation = $self->getRelation();
1998 my ($qset, $qname) = split(/\./, $qualifier);
2000 if ( exists($OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}) ) {
2001 $qualifier = $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'} || 'author';
2002 $log->debug("SRU toEvergreenAuth: $qset, $qname $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'}\n");
2005 return { qualifier => $qualifier, term => $term };
2010 sub sru_auth_search {
2013 my $req = SRU::Request->newFromCGI( $cgi );
2014 my $resp = SRU::Response->newFromRequest( $req );
2016 if ( $resp->type eq 'searchRetrieve' ) {
2017 return_auth_response($cgi, $req, $resp);
2018 } elsif ( $resp->type eq 'explain' ) {
2019 return_sru_explain($cgi, $req, $resp, \$auth_ex_doc,
2020 \%OpenILS::WWW::SuperCat::nested_auth_qualifier_map,
2021 \%OpenILS::WWW::SuperCat::qualifier_ids
2025 print $cgi->header( -type => 'application/xml' );
2026 print $U->entityize($resp->asXML) . "\n";
2027 return Apache2::Const::OK;
2030 sub explain_header {
2033 my $host = $cgi->virtual_host || $cgi->server_name;
2036 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
2037 my $rel_name = $cgi->url(-relative=>1);
2038 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
2040 my $base = $cgi->url(-base=>1);
2041 my $url = $cgi->url(-path_info=>$add_path);
2042 $url =~ s/^$base\///o;
2044 my $doc = $parser->parse_string($base_explain);
2045 my $e = $doc->documentElement;
2046 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
2047 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
2048 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
2053 sub return_sru_explain {
2054 my ($cgi, $req, $resp, $explain, $index_map, $qualifier_ids) = @_;
2056 $index_map ||= \%qualifier_map;
2058 my ($doc, $e) = explain_header($cgi);
2059 for my $name ( keys %{$index_map} ) {
2061 my $identifier = $qualifier_ids->{ $name };
2063 next unless $identifier;
2065 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
2066 $set_node->setAttribute( identifier => $identifier );
2067 $set_node->setAttribute( name => $name );
2069 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
2070 for my $index ( sort keys %{$index_map->{$name}} ) {
2071 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
2073 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
2074 $map_node->appendChild( $name_node );
2076 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
2078 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
2079 $index_node->appendChild( $title_node );
2080 $index_node->appendChild( $map_node );
2082 $index_node->setAttribute( id => "$name.$index" );
2083 $title_node->appendText($index_map->{$name}{$index}{'title'});
2084 $name_node->setAttribute( set => $name );
2085 $name_node->appendText($index_map->{$name}{$index}{'index'});
2087 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
2091 $$explain = $e->toString;
2095 SRU::Response::Record->new(
2096 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
2097 recordData => $$explain
2103 sub return_auth_response {
2104 my ($cgi, $req, $resp) = @_;
2106 my $cql_query = decode_utf8($req->query);
2107 my $search = $req->cql->toEvergreenAuth;
2109 my $qualifier = decode_utf8($search->{qualifier});
2110 my $term = decode_utf8($search->{term});
2112 $log->info("SRU NAF search string [$cql_query] converted to "
2113 . "[$qualifier:$term]\n");
2115 my $page_size = $req->maximumRecords;
2118 # startwith deals with pages, so convert startRecord to a page number
2119 my $page = ($req->startRecord / $page_size) || 0;
2122 if ($qualifier eq "id") {
2123 $recs = [ int($term) ];
2125 my ($realaxis, $refs) = ($qualifier =~ $authority_axis_re);
2127 my $method = "open-ils.supercat.authority.browse_top.by_axis";
2128 $method .= ".refs" if $refs;
2130 $recs = $supercat->request(
2139 my $record_position = $req->startRecord;
2140 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2141 foreach my $record (@$recs) {
2142 my $marcxml = $cstore->request(
2143 'open-ils.cstore.direct.authority.record_entry.retrieve', $record
2147 SRU::Response::Record->new(
2148 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
2149 recordData => $marcxml,
2150 recordPosition => ++$record_position
2155 $resp->numberOfRecords(scalar(@$recs));
2158 =head2 get_ou($org_unit)
2160 Returns an aou object for a given actor.org_unit shortname or ID.
2165 my $org = shift || '-';
2169 $org_unit = $actor->request(
2170 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2172 } elsif ($org !~ /^\d+$/o) {
2173 $org_unit = $actor->request(
2174 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2177 $org_unit = $actor->request(
2178 'open-ils.actor.org_unit_list.search' => id => $org