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 # last created first
1017 my @sorted_bucket_items = sort { $b->create_time cmp $a->create_time } @{ $bucket->items };
1019 my $feed = create_record_feed(
1022 [ map { $_->target_biblio_record_entry } @sorted_bucket_items ],
1024 $org_unit->[0]->shortname,
1029 $feed->id($bucket_tag);
1031 $feed->title("Items in Book Bag [".$bucket->name."]");
1032 $feed->description($bucket->description || ("Items in Book Bag [".$bucket->name."]"));
1033 $feed->creator($host);
1036 $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
1037 $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
1038 $feed->link(html => $base . "/html-full/$id" => 'text/html');
1039 $feed->link(unapi => $unapi);
1043 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1044 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
1049 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1050 print $U->entityize($feed->toString) . "\n";
1052 return Apache2::Const::OK;
1057 return Apache2::Const::DECLINED if (-e $apache->filename);
1061 my $year = (gmtime())[5] + 1900;
1062 my $host = $cgi->virtual_host || $cgi->server_name;
1065 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1066 my $rel_name = $cgi->url(-relative=>1);
1067 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1070 my $url = $cgi->url(-path_info=>$add_path);
1071 my $root = (split 'feed', $url)[0];
1072 my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
1073 my $unapi = (split 'feed', $url)[0] . 'unapi';
1075 my $skin = $cgi->param('skin') || 'default';
1076 my $locale = $cgi->param('locale') || 'en-US';
1077 my $org = $cgi->param('searchOrg');
1079 # Enable localized results of copy status, etc
1080 $supercat->session_locale($locale);
1082 my $org_unit = get_ou($org);
1083 my $scope = "l=" . $org_unit->[0]->id . "&";
1085 my $path = $cgi->path_info;
1086 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1088 $path =~ s/^\/(?:feed\/)?freshmeat\///og;
1090 my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
1091 my $flesh_feed = parse_feed_type($type);
1094 $limit = 10 if $limit !~ /^\d+$/;
1096 my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
1098 #if ($type eq 'opac') {
1099 # print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
1100 # join('&', map { "rl=" . $_ } @$list) .
1105 my $search = 'record';
1106 if ($rtype eq 'authority') {
1107 $search = 'authority';
1109 my $feed = create_record_feed( $search, $type, $list, $unapi, $org_unit->[0]->shortname, undef, $flesh_feed);
1113 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
1115 $feed->title("$limit most recent $rtype ${axis}s");
1118 $feed->creator($host);
1121 $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
1122 $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
1123 $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
1124 $feed->link(unapi => $unapi);
1128 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1129 join('&', map { 'rl=' . $_} @$list ),
1134 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1135 print $U->entityize($feed->toString) . "\n";
1137 return Apache2::Const::OK;
1140 sub opensearch_osd {
1141 my $version = shift;
1146 if ($version eq '1.0') {
1148 Content-type: application/opensearchdescription+xml; charset=utf-8
1150 <?xml version="1.0" encoding="UTF-8"?>
1151 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
1152 <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&startPage={startPage}&startIndex={startIndex}&count={count}</Url>
1153 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
1154 <ShortName>$lib</ShortName>
1155 <LongName>Search $lib</LongName>
1156 <Description>Search the $lib OPAC by $class.</Description>
1157 <Tags>$lib book library</Tags>
1158 <SampleSearch>harry+potter</SampleSearch>
1159 <Developer>Mike Rylander for GPLS/PINES</Developer>
1160 <Contact>feedback\@open-ils.org</Contact>
1161 <SyndicationRight>open</SyndicationRight>
1162 <AdultContent>false</AdultContent>
1163 </OpenSearchDescription>
1167 Content-type: application/opensearchdescription+xml; charset=utf-8
1169 <?xml version="1.0" encoding="UTF-8"?>
1170 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
1171 <ShortName>$lib</ShortName>
1172 <Description>Search the $lib OPAC by $class.</Description>
1173 <Tags>$lib book library</Tags>
1174 <Url type="application/rss+xml"
1175 template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1176 <Url type="application/atom+xml"
1177 template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1178 <Url type="application/x-mods3+xml"
1179 template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1180 <Url type="application/x-mods+xml"
1181 template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1182 <Url type="application/x-marcxml+xml"
1183 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1184 <Url type="text/html"
1185 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1186 <LongName>Search $lib</LongName>
1187 <Query role="example" searchTerms="harry+potter" />
1188 <Developer>Mike Rylander for GPLS/PINES</Developer>
1189 <Contact>feedback\@open-ils.org</Contact>
1190 <SyndicationRight>open</SyndicationRight>
1191 <AdultContent>false</AdultContent>
1192 <Language>en-US</Language>
1193 <OutputEncoding>UTF-8</OutputEncoding>
1194 <InputEncoding>UTF-8</InputEncoding>
1195 </OpenSearchDescription>
1199 return Apache2::Const::OK;
1202 sub opensearch_feed {
1204 return Apache2::Const::DECLINED if (-e $apache->filename);
1207 my $year = (gmtime())[5] + 1900;
1209 my $host = $cgi->virtual_host || $cgi->server_name;
1212 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1213 my $rel_name = $cgi->url(-relative=>1);
1214 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1217 my $url = $cgi->url(-path_info=>$add_path);
1218 my $root = (split 'opensearch', $url)[0];
1219 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1220 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1222 my $path = $cgi->path_info;
1223 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1225 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1231 if (!$lib || $lib eq '-') {
1232 $lib = $actor->request(
1233 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1234 )->gather(1)->[0]->shortname;
1237 if ($class eq '-') {
1241 return opensearch_osd($version, $lib, $class, $base);
1245 my $page = $cgi->param('startPage') || 1;
1246 my $offset = $cgi->param('startIndex') || 1;
1247 my $limit = $cgi->param('count') || 10;
1249 $page = 1 if ($page !~ /^\d+$/);
1250 $offset = 1 if ($offset !~ /^\d+$/);
1251 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1254 $offset = ($page - 1) * $limit;
1259 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1260 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1262 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1263 $lang = '' if ($lang eq '*');
1265 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1267 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1270 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1271 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1273 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1276 $type = $cgi->param('responseType') if $cgi->param('responseType');
1279 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1283 my $kwt = $cgi->param('kw');
1284 my $tit = $cgi->param('ti');
1285 my $aut = $cgi->param('au');
1286 my $sut = $cgi->param('su');
1287 my $set = $cgi->param('se');
1289 $terms .= " " if ($terms && $kwt);
1290 $terms .= "keyword: $kwt" if ($kwt);
1291 $terms .= " " if ($terms && $tit);
1292 $terms .= "title: $tit" if ($tit);
1293 $terms .= " " if ($terms && $aut);
1294 $terms .= "author: $aut" if ($aut);
1295 $terms .= " " if ($terms && $sut);
1296 $terms .= "subject: $sut" if ($sut);
1297 $terms .= " " if ($terms && $set);
1298 $terms .= "series: $set" if ($set);
1300 if ($version eq '1.0') {
1302 } elsif ($type eq '-') {
1305 my $flesh_feed = parse_feed_type($type);
1307 $terms = decode_utf8($terms);
1308 $lang = 'eng' if ($lang eq 'en-US');
1310 $log->debug("OpenSearch terms: $terms");
1312 my $org_unit = get_ou($org);
1314 # Apostrophes break search and get indexed as spaces anyway
1315 my $safe_terms = $terms;
1316 $safe_terms =~ s{'}{ }go;
1318 my $recs = $search->request(
1319 'open-ils.search.biblio.multiclass.query' => {
1320 org_unit => $org_unit->[0]->id,
1324 sort_dir => $sortdir,
1325 default_class => $class,
1326 ($lang ? ( 'language' => $lang ) : ()),
1327 } => $safe_terms => 1
1330 $log->debug("Hits for [$terms]: $recs->{count}");
1332 my $feed = create_record_feed(
1335 [ map { $_->[0] } @{$recs->{ids}} ],
1342 $log->debug("Feed created...");
1346 $feed->search($safe_terms);
1347 $feed->class($class);
1349 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1351 $feed->creator($host);
1354 $feed->_create_node(
1355 $feed->{item_xpath},
1356 'http://a9.com/-/spec/opensearch/1.1/',
1361 $feed->_create_node(
1362 $feed->{item_xpath},
1363 'http://a9.com/-/spec/opensearch/1.1/',
1368 $feed->_create_node(
1369 $feed->{item_xpath},
1370 'http://a9.com/-/spec/opensearch/1.1/',
1375 $log->debug("...basic feed data added...");
1379 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1380 'application/opensearch+xml'
1381 ) if ($offset + $limit < $recs->{count});
1385 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1386 'application/opensearch+xml'
1391 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1392 'application/opensearch+xml'
1397 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1398 'application/rss+xml'
1403 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1404 'application/atom+xml'
1409 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1415 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1419 $feed->link( 'unapi-server' => $unapi);
1421 $log->debug("...feed links added...");
1425 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1426 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1430 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1431 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1433 $log->debug("...and feed returned.");
1435 return Apache2::Const::OK;
1438 sub create_record_feed {
1441 my $records = shift;
1444 my $lib = uc(shift()) || '-';
1451 my $base = $cgi->url;
1452 my $host = $cgi->virtual_host || $cgi->server_name;
1454 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
1458 my $tag_prefix = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d", $month, $day);
1460 my $flesh_feed = defined($flesh) ? $flesh : parse_feed_type($type);
1462 $type =~ s/(-full|-uris)$//o;
1464 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1465 $feed->base($base) if ($flesh);
1466 $feed->unapi($unapi) if ($flesh);
1468 $type = 'atom' if ($type eq 'html');
1469 $type = 'marcxml' if (($type eq 'htmlholdings') || ($type eq 'marctxt') || ($type eq 'ris'));
1471 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1474 for my $record (@$records) {
1475 next unless($record);
1477 #my $rec = $record->id;
1480 my $item_tag = "$tag_prefix:biblio-record_entry/$rec/$lib";
1481 $item_tag = "$tag_prefix:metabib-metarecord/$rec/$lib" if ($search eq 'metarecord');
1482 $item_tag = "$tag_prefix:isbn/$rec/$lib" if ($search eq 'isbn');
1483 $item_tag .= "/$depth" if (defined($depth));
1485 $item_tag = "$tag_prefix:authority-record_entry/$rec" if ($search eq 'authority');
1487 my $xml = $supercat->request(
1488 "open-ils.supercat.$search.$type.retrieve",
1493 my $node = $feed->add_item($xml);
1497 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && ($flesh > 0)) {
1498 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
1499 while ( !$r->complete ) {
1500 $xml .= join('', map {$_->content} $r->recv);
1502 $xml .= join('', map {$_->content} $r->recv);
1503 $node->add_holdings($xml);
1506 $node->id($item_tag);
1507 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1508 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
1509 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
1510 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1511 $node->link('unapi-id' => $item_tag) if ($flesh);
1519 return Apache2::Const::DECLINED if (-e $apache->filename);
1522 my $year = (gmtime())[5] + 1900;
1524 my $host = $cgi->virtual_host || $cgi->server_name;
1527 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1528 my $rel_name = $cgi->url(-relative=>1);
1529 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1532 my $url = $cgi->url(-path_info=>$add_path);
1533 my $root = (split 'browse', $url)[0];
1534 my $base = (split 'browse', $url)[0] . 'browse';
1535 my $unapi = (split 'browse', $url)[0] . 'unapi';
1537 my $path = $cgi->path_info;
1540 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1541 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1543 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1545 my $status = [$cgi->param('status')];
1546 my $cpLoc = [$cgi->param('copyLocation')];
1547 $site ||= $cgi->param('searchOrg');
1548 $page ||= $cgi->param('startPage') || 0;
1549 $page_size ||= $cgi->param('count') || 9;
1551 $page = 0 if ($page !~ /^-?\d+$/);
1552 $page_size = 9 if $page_size !~ /^\d+$/;
1554 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1555 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1557 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1558 warn "something's wrong...";
1559 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1563 $string = decode_utf8($string);
1564 $string =~ s/\+/ /go;
1568 if ($axis =~ /^authority/) {
1569 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1571 my $method = "open-ils.supercat.authority.browse_center.by_axis";
1572 $method .= ".refs" if $refs;
1574 $tree = $supercat->request(
1582 $tree = $supercat->request(
1583 "open-ils.supercat.$axis.browse",
1593 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1595 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1596 print $header.$content;
1597 return Apache2::Const::OK;
1600 sub string_startwith {
1602 return Apache2::Const::DECLINED if (-e $apache->filename);
1605 my $year = (gmtime())[5] + 1900;
1607 my $host = $cgi->virtual_host || $cgi->server_name;
1610 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1611 my $rel_name = $cgi->url(-relative=>1);
1612 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1615 my $url = $cgi->url(-path_info=>$add_path);
1616 my $root = (split 'startwith', $url)[0];
1617 my $base = (split 'startwith', $url)[0] . 'startwith';
1618 my $unapi = (split 'startwith', $url)[0] . 'unapi';
1620 my $path = $cgi->path_info;
1623 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1624 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1626 my $status = [$cgi->param('status')];
1627 my $cpLoc = [$cgi->param('copyLocation')];
1628 $site ||= $cgi->param('searchOrg');
1629 $page ||= $cgi->param('startPage') || 0;
1630 $page_size ||= $cgi->param('count') || 9;
1632 $page = 0 if ($page !~ /^-?\d+$/);
1633 $page_size = 9 if $page_size !~ /^\d+$/;
1635 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1636 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1638 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1639 warn "something's wrong...";
1640 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1644 $string = decode_utf8($string);
1645 $string =~ s/\+/ /go;
1649 if ($axis =~ /^authority/) {
1650 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1652 my $method = "open-ils.supercat.authority.browse_top.by_axis";
1653 $method .= ".refs" if $refs;
1655 $tree = $supercat->request(
1663 $tree = $supercat->request(
1664 "open-ils.supercat.$axis.startwith",
1674 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1676 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1677 print $header.$content;
1678 return Apache2::Const::OK;
1681 sub item_age_browse {
1683 return Apache2::Const::DECLINED if (-e $apache->filename);
1686 my $year = (gmtime())[5] + 1900;
1688 my $host = $cgi->virtual_host || $cgi->server_name;
1691 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1692 my $rel_name = $cgi->url(-relative=>1);
1693 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1696 my $url = $cgi->url(-path_info=>$add_path);
1697 my $root = (split 'browse', $url)[0];
1698 my $base = (split 'browse', $url)[0] . 'browse';
1699 my $unapi = (split 'browse', $url)[0] . 'unapi';
1701 my $path = $cgi->path_info;
1704 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1705 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1707 unless ($axis eq 'item-age') {
1708 warn "something's wrong...";
1709 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1713 my $status = [$cgi->param('status')];
1714 my $cpLoc = [$cgi->param('copyLocation')];
1715 $site ||= $cgi->param('searchOrg') || '-';
1716 $page ||= $cgi->param('startPage') || 1;
1717 $page_size ||= $cgi->param('count') || 10;
1719 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1720 $page_size = 10 if $page_size !~ /^\d+$/;
1722 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1723 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1725 my $recs = $supercat->request(
1726 "open-ils.supercat.new_book_list",
1734 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1736 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1737 print $header.$content;
1738 return Apache2::Const::OK;
1741 our %qualifier_ids = (
1742 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1743 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1744 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1748 # Our authority search options are currently pretty impoverished;
1749 # just right-truncated string match on a few categories, or by
1751 our %nested_auth_qualifier_map = (
1753 id => { index => 'id', title => 'Record number'},
1754 name => { index => 'author', title => 'Personal or corporate author, or meeting name'},
1755 title => { index => 'title', title => 'Uniform title'},
1756 subject => { index => 'subject', title => 'Chronological term, topical term, geographic name, or genre/form term'},
1757 topic => { index => 'topic', title => 'Topical term'},
1761 my $base_explain = <<XML;
1763 id="evergreen-sru-explain-full"
1764 authoritative="true"
1765 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1766 xmlns="http://explain.z3950.org/dtd/2.0/">
1767 <serverInfo transport="http" protocol="SRU" version="1.1">
1774 <title primary="true"/>
1775 <description primary="true"/>
1779 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1784 identifier="info:srw/schema/1/marcxml-v1.1"
1785 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1789 <title>MARC21Slim (marcxml)</title>
1794 <default type="numberOfRecords">10</default>
1795 <default type="contextSet">eg</default>
1796 <default type="index">keyword</default>
1797 <default type="relation">all</default>
1798 <default type="sortSchema">marcxml</default>
1799 <default type="retrieveSchema">marcxml</default>
1800 <setting type="maximumRecords">50</setting>
1801 <supports type="relationModifier">relevant</supports>
1802 <supports type="relationModifier">stem</supports>
1803 <supports type="relationModifier">fuzzy</supports>
1804 <supports type="relationModifier">word</supports>
1815 my $req = SRU::Request->newFromCGI( $cgi );
1816 my $resp = SRU::Response->newFromRequest( $req );
1818 # Find the org_unit shortname, if passed as part of the URL
1819 # http://example.com/opac/extras/sru/SHORTNAME
1820 my $url = $cgi->path_info;
1821 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1823 if ( $resp->type eq 'searchRetrieve' ) {
1825 # Older versions of Debian packages returned terms to us double-encoded,
1826 # so we had to forcefully double-decode them a second time with
1827 # an outer decode('utf8', $string) call; this seems to be resolved with
1828 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1829 my $cql_query = decode_utf8($req->query);
1830 my $search_string = decode_utf8($req->cql->toEvergreen);
1832 # Ensure the search string overrides the default site
1833 if ($shortname and $search_string !~ m#site:#) {
1834 $search_string .= " site:$shortname";
1837 my $offset = $req->startRecord;
1838 $offset-- if ($offset);
1841 my $limit = $req->maximumRecords;
1844 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1846 my $recs = $search->request(
1847 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1850 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1852 foreach my $record (@$bre) {
1853 my $marcxml = $record->marc;
1854 # Make the beast conform to a VDX-supported format
1855 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1856 # Trying to implement LIBSOL_852_A format; so much for standards
1858 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1859 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1861 # Force record leader to 'a' as our data is always UTF8
1862 # Avoids marc8_to_utf8 from being invoked with horrible results
1863 # on the off-chance the record leader isn't correct
1864 my $ldr = $marc->leader;
1865 substr($ldr, 9, 1, 'a');
1866 $marc->leader($ldr);
1868 # Expects the record ID in the 001
1869 $marc->delete_field($_) for ($marc->field('001'));
1870 if (!$marc->field('001')) {
1871 $marc->insert_fields_ordered(
1872 MARC::Field->new( '001', $record->id )
1875 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1876 foreach my $cn (keys %$bib_holdings) {
1877 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1878 $marc->insert_fields_ordered(
1881 a => $cp->{'location'},
1882 b => $bib_holdings->{$cn}->{'owning_lib'},
1884 d => $cp->{'circlib'},
1885 g => $cp->{'barcode'},
1886 n => $cp->{'status'},
1892 # Ensure the data is encoded as UTF8 before we hand it off
1893 $marcxml = encode_utf8($marc->as_xml_record());
1894 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1898 SRU::Response::Record->new(
1899 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1900 recordData => $marcxml,
1901 recordPosition => ++$offset
1906 $resp->numberOfRecords($recs->{count});
1908 } elsif ( $resp->type eq 'explain' ) {
1909 return_sru_explain($cgi, $req, $resp, \$ex_doc,
1911 \%OpenILS::WWW::SuperCat::qualifier_ids
1915 SRU::Response::Record->new(
1916 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1917 recordData => $ex_doc
1922 print $cgi->header( -type => 'application/xml' );
1923 print $U->entityize($resp->asXML) . "\n";
1924 return Apache2::Const::OK;
1929 package CQL::BooleanNode;
1933 my $left = $self->left();
1934 my $right = $self->right();
1935 my $leftStr = $left->toEvergreen;
1936 my $rightStr = $right->toEvergreen();
1938 my $op = '||' if uc $self->op() eq 'OR';
1941 return "$leftStr $rightStr";
1944 sub toEvergreenAuth {
1945 return toEvergreen(shift);
1948 package CQL::TermNode;
1952 my $qualifier = $self->getQualifier();
1953 my $term = $self->getTerm();
1954 my $relation = $self->getRelation();
1958 my ($qset, $qname) = split(/\./, $qualifier);
1960 # Per http://www.loc.gov/standards/sru/specs/cql.html
1961 # "All parts of CQL are case insensitive [...] If any case insensitive
1962 # part of CQL is specified with both upper and lower case, it is for
1963 # aesthetic purposes only."
1965 # So fold the qualifier and relation to lower case
1967 $qname = lc($qname);
1969 if ( exists($qualifier_map{$qset}{$qname}) ) {
1970 $qualifier = $qualifier_map{$qset}{$qname}{'index'} || 'kw';
1971 $log->debug("SRU toEvergreen: $qset, $qname $qualifier_map{$qset}{$qname}{'index'}\n");
1974 my @modifiers = $relation->getModifiers();
1976 my $base = $relation->getBase();
1977 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
1980 foreach my $m ( @modifiers ) {
1981 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
1987 $quote_it = 0 if ( $base eq 'all' );
1988 $term = maybeQuote($term) if $quote_it;
1991 croak( "Evergreen doesn't support the $base relations" );
1999 return "$qualifier:$term";
2002 sub toEvergreenAuth {
2004 my $qualifier = $self->getQualifier();
2005 my $term = $self->getTerm();
2006 my $relation = $self->getRelation();
2010 my ($qset, $qname) = split(/\./, $qualifier);
2012 if ( exists($OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}) ) {
2013 $qualifier = $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'} || 'author';
2014 $log->debug("SRU toEvergreenAuth: $qset, $qname $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'}\n");
2017 return { qualifier => $qualifier, term => $term };
2022 sub sru_auth_search {
2025 my $req = SRU::Request->newFromCGI( $cgi );
2026 my $resp = SRU::Response->newFromRequest( $req );
2028 if ( $resp->type eq 'searchRetrieve' ) {
2029 return_auth_response($cgi, $req, $resp);
2030 } elsif ( $resp->type eq 'explain' ) {
2031 return_sru_explain($cgi, $req, $resp, \$auth_ex_doc,
2032 \%OpenILS::WWW::SuperCat::nested_auth_qualifier_map,
2033 \%OpenILS::WWW::SuperCat::qualifier_ids
2037 print $cgi->header( -type => 'application/xml' );
2038 print $U->entityize($resp->asXML) . "\n";
2039 return Apache2::Const::OK;
2042 sub explain_header {
2045 my $host = $cgi->virtual_host || $cgi->server_name;
2048 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
2049 my $rel_name = $cgi->url(-relative=>1);
2050 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
2052 my $base = $cgi->url(-base=>1);
2053 my $url = $cgi->url(-path_info=>$add_path);
2054 $url =~ s/^$base\///o;
2056 my $doc = $parser->parse_string($base_explain);
2057 my $e = $doc->documentElement;
2058 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
2059 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
2060 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
2065 sub return_sru_explain {
2066 my ($cgi, $req, $resp, $explain, $index_map, $qualifier_ids) = @_;
2068 $index_map ||= \%qualifier_map;
2070 my ($doc, $e) = explain_header($cgi);
2071 for my $name ( keys %{$index_map} ) {
2073 my $identifier = $qualifier_ids->{ $name };
2075 next unless $identifier;
2077 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
2078 $set_node->setAttribute( identifier => $identifier );
2079 $set_node->setAttribute( name => $name );
2081 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
2082 for my $index ( sort keys %{$index_map->{$name}} ) {
2083 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
2085 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
2086 $map_node->appendChild( $name_node );
2088 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
2090 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
2091 $index_node->appendChild( $title_node );
2092 $index_node->appendChild( $map_node );
2094 $index_node->setAttribute( id => "$name.$index" );
2095 $title_node->appendText($index_map->{$name}{$index}{'title'});
2096 $name_node->setAttribute( set => $name );
2097 $name_node->appendText($index_map->{$name}{$index}{'index'});
2099 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
2103 $$explain = $e->toString;
2107 SRU::Response::Record->new(
2108 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
2109 recordData => $$explain
2115 sub return_auth_response {
2116 my ($cgi, $req, $resp) = @_;
2118 my $cql_query = decode_utf8($req->query);
2119 my $search = $req->cql->toEvergreenAuth;
2121 my $qualifier = decode_utf8($search->{qualifier});
2122 my $term = decode_utf8($search->{term});
2124 $log->info("SRU NAF search string [$cql_query] converted to "
2125 . "[$qualifier:$term]\n");
2127 my $page_size = $req->maximumRecords;
2130 # startwith deals with pages, so convert startRecord to a page number
2131 my $page = ($req->startRecord / $page_size) || 0;
2134 if ($qualifier eq "id") {
2135 $recs = [ int($term) ];
2137 my ($realaxis, $refs) = ($qualifier =~ $authority_axis_re);
2139 my $method = "open-ils.supercat.authority.browse_top.by_axis";
2140 $method .= ".refs" if $refs;
2142 $recs = $supercat->request(
2151 my $record_position = $req->startRecord;
2152 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2153 foreach my $record (@$recs) {
2154 my $marcxml = $cstore->request(
2155 'open-ils.cstore.direct.authority.record_entry.retrieve', $record
2159 SRU::Response::Record->new(
2160 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
2161 recordData => $marcxml,
2162 recordPosition => ++$record_position
2167 $resp->numberOfRecords(scalar(@$recs));
2170 =head2 get_ou($org_unit)
2172 Returns an aou object for a given actor.org_unit shortname or ID.
2177 my $org = shift || '-';
2181 $org_unit = $actor->request(
2182 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2184 } elsif ($org !~ /^\d+$/o) {
2185 $org_unit = $actor->request(
2186 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2189 $org_unit = $actor->request(
2190 'open-ils.actor.org_unit_list.search' => id => $org