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",
274 return Apache2::Const::OK;
277 sub check_child_init() {
278 if (!defined $supercat || !defined $actor || !defined $search) {
279 # For some reason one (or more) of our appsessions is missing....
285 =head2 parse_feed_type($type)
287 Determines whether and how a given feed type needs to be "fleshed out"
288 with holdings information.
290 The feed type could end with the string "-full", in which case we want
291 to return call numbers, copies, and URIS.
293 Or the feed type could end with "-uris", in which case we want to return
294 call numbers and URIS.
296 Otherwise, we won't return any holdings.
300 sub parse_feed_type {
301 my $type = shift || '';
303 if ($type =~ /-full$/o) {
307 if ($type =~ /-uris$/o) {
311 # Otherwise, we'll return just the facts, ma'am
315 =head2 supercat_format($format_hashref, $format_type)
317 Given a reference to a hash containing the namespace_uri,
318 docs, and schema location attributes for a set of formats,
319 generate the XML description required by the supercat service.
321 We derive the base type from the format type so that we do not
322 have to populate the hash with redundant information.
326 sub supercat_format {
330 (my $base_type = $type) =~ s/(-full|-uris)$//o;
332 my $format = "<format><name>$type</name><type>application/xml</type>";
334 for my $part ( qw/namespace_uri docs schema_location/ ) {
335 $format .= "<$part>$$h{$base_type}{$part}</$part>"
336 if ($$h{$base_type}{$part});
339 $format .= '</format>';
344 =head2 unapi_format($format_hashref, $format_type)
346 Given a reference to a hash containing the namespace_uri,
347 docs, and schema location attributes for a set of formats,
348 generate the XML description required by the supercat service.
350 We derive the base type from the format type so that we do not
351 have to populate the hash with redundant information.
359 (my $base_type = $type) =~ s/(-full|-uris)$//o;
361 my $format = "<format name='$type' type='application/xml'";
363 for my $part ( qw/namespace_uri docs schema_location/ ) {
364 $format .= " $part='$$h{$base_type}{$part}'"
365 if ($$h{$base_type}{$part});
377 return Apache2::Const::DECLINED if (-e $apache->filename);
381 (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
384 ->request("open-ils.supercat.oisbn", $isbn)
387 print "Content-type: application/xml; charset=utf-8\n\n";
388 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
390 unless (exists $$list{metarecord}) {
392 return Apache2::Const::OK;
395 print "<idlist metarecord='$$list{metarecord}'>\n";
397 for ( keys %{ $$list{record_list} } ) {
398 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
399 print " <isbn record='$_'>$o</isbn>\n"
404 return Apache2::Const::OK;
410 return Apache2::Const::DECLINED if (-e $apache->filename);
417 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
418 my $rel_name = $cgi->url(-relative=>1);
419 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
422 my $url = $cgi->url(-path_info=>$add_path);
423 my $root = (split 'unapi', $url)[0];
424 my $base = (split 'unapi', $url)[0] . 'unapi';
427 my $uri = $cgi->param('id') || '';
428 my $host = $cgi->virtual_host || $cgi->server_name;
430 my $skin = $cgi->param('skin') || 'default';
431 my $locale = $cgi->param('locale') || 'en-US';
433 # Enable localized results of copy status, etc
434 $supercat->session_locale($locale);
436 my $format = $cgi->param('format') || '';
437 my $flesh_feed = parse_feed_type($format);
438 (my $base_format = $format) =~ s/(-full|-uris)$//o;
439 my ($id,$type,$command,$lib,$depth,$paging) = ('','record','');
440 my $body = "Content-type: application/xml; charset=utf-8\n\n";
442 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
445 ($lib,$depth) = split('/', $4);
446 $type = 'metarecord' if ($1 =~ /^m/o);
447 $type = 'authority' if ($1 =~ /^authority/o);
451 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
454 ->request("open-ils.supercat.$type.formats")
457 if ($type eq 'record' or $type eq 'isbn') {
458 $body .= <<" FORMATS";
460 <format name='opac' type='text/html'/>
461 <format name='html' type='text/html'/>
462 <format name='htmlholdings' type='text/html'/>
463 <format name='holdings_xml' type='application/xml'/>
464 <format name='holdings_xml-full' type='application/xml'/>
465 <format name='html-full' type='text/html'/>
466 <format name='htmlholdings-full' type='text/html'/>
467 <format name='marctxt' type='text/plain'/>
468 <format name='ris' type='text/plain'/>
470 } elsif ($type eq 'metarecord') {
471 $body .= <<" FORMATS";
473 <format name='opac' type='text/html'/>
476 $body .= <<" FORMATS";
482 my ($type) = keys %$h;
483 $body .= unapi_format($h, $type);
485 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
486 $body .= unapi_format($h, "$type-full");
487 $body .= unapi_format($h, "$type-uris");
491 $body .= "</formats>\n";
495 ->request("open-ils.supercat.$type.formats")
500 ->request("open-ils.supercat.metarecord.formats")
504 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
505 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
507 $body .= <<" FORMATS";
509 <format name='opac' type='text/html'/>
510 <format name='html' type='text/html'/>
511 <format name='htmlholdings' type='text/html'/>
512 <format name='holdings_xml' type='application/xml'/>
513 <format name='holdings_xml-full' type='application/xml'/>
514 <format name='html-full' type='text/html'/>
515 <format name='htmlholdings-full' type='text/html'/>
516 <format name='marctxt' type='text/plain'/>
517 <format name='ris' type='text/plain'/>
522 my ($type) = keys %$h;
523 $body .= "\t" . unapi_format($h, $type);
525 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
526 $body .= "\t" . unapi_format($h, "$type-full");
527 $body .= "\t" . unapi_format($h, "$type-uris");
531 $body .= "</formats>\n";
535 return Apache2::Const::OK;
539 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
543 ($lib,$depth) = split('/', $4);
545 $type = 'metarecord' if ($scheme =~ /^metabib/o);
546 $type = 'isbn' if ($scheme =~ /^isbn/o);
547 $type = 'acp' if ($scheme =~ /^asset-copy/o);
548 $type = 'acn' if ($scheme =~ /^asset-call_number/o);
549 $type = 'auri' if ($scheme =~ /^asset-uri/o);
550 $type = 'authority' if ($scheme =~ /^authority/o);
551 $command = 'retrieve';
552 $command = 'browse' if (grep { $scheme eq $_ } qw/call_number title author subject topic authority.title authority.author authority.subject authority.topic series item-age/);
553 $command = 'browse' if ($scheme =~ /^authority/);
557 $paging = [split ',', $paging];
562 if (!$lib || $lib eq '-') {
563 $lib = $actor->request(
564 'open-ils.actor.org_unit_list.search' => parent_ou => undef
565 )->gather(1)->[0]->shortname;
568 my ($lib_object,$lib_id,$ou_types,$lib_depth);
569 if ($type ne 'acn' && $type ne 'acp' && $type ne 'auri') {
570 $lib_object = $actor->request(
571 'open-ils.actor.org_unit_list.search' => shortname => $lib
573 $lib_id = $lib_object->id;
575 $ou_types = $actor->request( 'open-ils.actor.org_types.retrieve' )->gather(1);
576 $lib_depth = defined($depth) ? $depth : (grep { $_->id == $lib_object->ou_type } @$ou_types)[0]->depth;
579 if ($command eq 'browse') {
580 print "Location: $root/browse/$base_format/$scheme/$lib/$id\n\n";
584 if ($type eq 'isbn') {
585 my $rec = $supercat->request('open-ils.supercat.isbn.object.retrieve',$id)->gather(1);
587 # Escape user input before display
588 $command = CGI::escapeHTML($command);
589 $id = CGI::escapeHTML($id);
590 $type = CGI::escapeHTML($type);
591 $format = CGI::escapeHTML(decode_utf8($format));
593 print "Content-type: text/html; charset=utf-8\n\n";
594 $apache->custom_response( 404, <<" HTML");
597 <title>Type [$type] with id [$id] not found!</title>
601 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
612 { (keys(%$_))[0] eq $base_format }
613 @{ $supercat->request("open-ils.supercat.$type.formats")->gather(1) }
615 { $_ eq $base_format }
616 qw/opac html htmlholdings marctxt ris holdings_xml/
618 # Escape user input before display
619 $format = CGI::escapeHTML($format);
620 $type = CGI::escapeHTML($type);
622 print "Content-type: text/html; charset=utf-8\n\n";
623 $apache->custom_response( 406, <<" HTML");
626 <title>Invalid format [$format] for type [$type]!</title>
630 <center>Sorry, format $format is not valid for type $type.</center>
637 if ($format eq 'opac') {
638 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id&l=$lib_id&d=$lib_depth\n\n"
639 if ($type eq 'metarecord');
640 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id&l=$lib_id&d=$lib_depth\n\n"
641 if ($type eq 'record');
643 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format) && ($type ne 'acn' && $type ne 'acp' && $type ne 'auri')) {
644 my $feed = create_record_feed(
655 # Escape user input before display
656 $command = CGI::escapeHTML($command);
657 $id = CGI::escapeHTML($id);
658 $type = CGI::escapeHTML($type);
659 $format = CGI::escapeHTML(decode_utf8($format));
661 print "Content-type: text/html; charset=utf-8\n\n";
662 $apache->custom_response( 404, <<" HTML");
665 <title>Type [$type] with id [$id] not found!</title>
669 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
677 $feed->creator($host);
679 $feed->link( unapi => $base) if ($flesh_feed);
681 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
682 print $feed->toString . "\n";
684 return Apache2::Const::OK;
687 my $method = "open-ils.supercat.$type.$base_format.$command";
689 push @params, $lib, $lib_depth, $flesh_feed, $paging if ($base_format eq 'holdings_xml');
691 # for acn, acp, etc, the "lib" pathinfo position isn't useful.
692 # however, we can have it carry extra options like no_record! (comma separated)
693 push @params, { map { ( $_ => 1 ) } split(',', $lib) } if ( grep { $type eq $_} qw/acn acp auri/);
695 my $req = $supercat->request($method,@params);
696 my $data = $req->gather();
698 if ($req->failed || !$data) {
699 # Escape user input before display
700 $command = CGI::escapeHTML($command);
701 $id = CGI::escapeHTML($id);
702 $type = CGI::escapeHTML($type);
703 $format = CGI::escapeHTML(decode_utf8($format));
705 print "Content-type: text/html; charset=utf-8\n\n";
706 $apache->custom_response( 404, <<" HTML");
709 <title>$type $id not found!</title>
713 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
720 print "Content-type: application/xml; charset=utf-8\n\n";
722 # holdings_xml format comes back to us without an XML declaration
723 # and without being entityized; fix that here
724 if ($base_format eq 'holdings_xml') {
725 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
726 print $U->entityize($data);
728 while (my $c = $req->recv) {
729 print $U->entityize($c->content);
735 return Apache2::Const::OK;
741 return Apache2::Const::DECLINED if (-e $apache->filename);
748 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
749 my $rel_name = $cgi->url(-relative=>1);
750 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
753 my $url = $cgi->url(-path_info=>$add_path);
754 my $root = (split 'supercat', $url)[0];
755 my $base = (split 'supercat', $url)[0] . 'supercat';
756 my $unapi = (split 'supercat', $url)[0] . 'unapi';
758 my $host = $cgi->virtual_host || $cgi->server_name;
760 my $path = $cgi->path_info;
761 my ($id,$type,$format,$command) = reverse split '/', $path;
762 my $flesh_feed = parse_feed_type($format);
763 (my $base_format = $format) =~ s/(-full|-uris)$//o;
765 my $skin = $cgi->param('skin') || 'default';
766 my $locale = $cgi->param('locale') || 'en-US';
768 # Enable localized results of copy status, etc
769 $supercat->session_locale($locale);
771 if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
772 print "Content-type: application/xml; charset=utf-8\n";
775 ->request("open-ils.supercat.$1.formats")
783 <type>text/html</type>
786 if ($1 eq 'record' or $1 eq 'isbn') {
788 <name>htmlholdings</name>
789 <type>text/html</type>
793 <type>text/html</type>
796 <name>htmlholdings-full</name>
797 <type>text/html</type>
800 <name>html-full</name>
801 <type>text/html</type>
805 <type>text/plain</type>
809 <type>text/plain</type>
814 my ($type) = keys %$h;
815 print supercat_format($h, $type);
817 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
818 print supercat_format($h, "$type-full");
819 print supercat_format($h, "$type-uris");
824 print "</formats>\n";
826 return Apache2::Const::OK;
830 ->request("open-ils.supercat.record.formats")
835 ->request("open-ils.supercat.metarecord.formats")
839 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
840 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
845 <type>text/html</type>
848 <name>htmlholdings</name>
849 <type>text/html</type>
853 <type>text/html</type>
856 <name>htmlholdings-full</name>
857 <type>text/html</type>
860 <name>html-full</name>
861 <type>text/html</type>
865 <type>text/plain</type>
869 <type>text/plain</type>
873 my ($type) = keys %$h;
874 print supercat_format($h, $type);
876 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
877 print supercat_format($h, "$type-full");
878 print supercat_format($h, "$type-uris");
883 print "</formats>\n";
886 return Apache2::Const::OK;
889 if ($format eq 'opac') {
890 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id\n\n"
891 if ($type eq 'metarecord');
892 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id\n\n"
893 if ($type eq 'record');
896 } elsif ($base_format eq 'marc21') {
900 my $bib = $supercat->request( "open-ils.supercat.record.object.retrieve", $id )->gather(1)->[0];
902 print "Content-type: application/octet-stream\n\n" . MARC::Record->new_from_xml( $bib->marc, 'UTF-8', 'USMARC' )->as_usmarc;
907 # Escape user input before display
908 $id = CGI::escapeHTML($id);
910 print "Content-type: text/html; charset=utf-8\n\n";
911 $apache->custom_response( 404, <<" HTML");
918 <center>Couldn't fetch $id as MARC21.</center>
925 return Apache2::Const::OK;
927 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
928 my $feed = create_record_feed(
936 $feed->creator($host);
940 $feed->link( unapi => $base) if ($flesh_feed);
942 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
943 print $feed->toString . "\n";
945 return Apache2::Const::OK;
948 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
952 # Escape user input before display
953 $command = CGI::escapeHTML($command);
954 $id = CGI::escapeHTML($id);
955 $type = CGI::escapeHTML($type);
956 $format = CGI::escapeHTML(decode_utf8($format));
958 print "Content-type: text/html; charset=utf-8\n\n";
959 $apache->custom_response( 404, <<" HTML");
962 <title>$type $id not found!</title>
966 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
973 print "Content-type: application/xml; charset=utf-8\n\n";
974 print $U->entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
976 return Apache2::Const::OK;
982 return Apache2::Const::DECLINED if (-e $apache->filename);
988 my $year = (gmtime())[5] + 1900;
989 my $host = $cgi->virtual_host || $cgi->server_name;
992 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
993 my $rel_name = $cgi->url(-relative=>1);
994 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
997 my $url = $cgi->url(-path_info=>$add_path);
998 my $root = (split 'feed', $url)[0] . '/';
999 my $base = (split 'bookbag', $url)[0] . '/bookbag';
1000 my $unapi = (split 'feed', $url)[0] . '/unapi';
1002 my $skin = $cgi->param('skin') || 'default';
1003 my $locale = $cgi->param('locale') || 'en-US';
1004 my $org = $cgi->param('searchOrg');
1006 # Enable localized results of copy status, etc
1007 $supercat->session_locale($locale);
1009 my $org_unit = get_ou($org);
1010 my $scope = "l=" . $org_unit->[0]->id . "&";
1012 $root =~ s{(?<!http:)//}{/}go;
1013 $base =~ s{(?<!http:)//}{/}go;
1014 $unapi =~ s{(?<!http:)//}{/}go;
1016 my $path = $cgi->path_info;
1017 #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
1019 my ($id,$type) = reverse split '/', $path;
1020 my $flesh_feed = parse_feed_type($type);
1022 my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
1023 return Apache2::Const::NOT_FOUND unless($bucket);
1025 my $bucket_tag = "tag:$host,$year:record_bucket/$id";
1026 if ($type eq 'opac') {
1027 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1028 join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
1033 # last created first
1034 my @sorted_bucket_items = sort { $b->create_time cmp $a->create_time } @{ $bucket->items };
1036 my $feed = create_record_feed(
1039 [ map { $_->target_biblio_record_entry } @sorted_bucket_items ],
1041 $org_unit->[0]->shortname,
1046 $feed->id($bucket_tag);
1048 $feed->title("Items in Book Bag [".$bucket->name."]");
1049 $feed->description($bucket->description || ("Items in Book Bag [".$bucket->name."]"));
1050 $feed->creator($host);
1053 $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
1054 $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
1055 $feed->link(html => $base . "/html-full/$id" => 'text/html');
1056 $feed->link(unapi => $unapi);
1060 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1061 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
1066 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1067 print $feed->toString . "\n";
1069 return Apache2::Const::OK;
1074 return Apache2::Const::DECLINED if (-e $apache->filename);
1080 my $year = (gmtime())[5] + 1900;
1081 my $host = $cgi->virtual_host || $cgi->server_name;
1084 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1085 my $rel_name = $cgi->url(-relative=>1);
1086 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1089 my $url = $cgi->url(-path_info=>$add_path);
1090 my $root = (split 'feed', $url)[0];
1091 my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
1092 my $unapi = (split 'feed', $url)[0] . 'unapi';
1094 my $skin = $cgi->param('skin') || 'default';
1095 my $locale = $cgi->param('locale') || 'en-US';
1096 my $org = $cgi->param('searchOrg');
1098 # Enable localized results of copy status, etc
1099 $supercat->session_locale($locale);
1101 my $org_unit = get_ou($org);
1102 my $scope = "l=" . $org_unit->[0]->id . "&";
1104 my $path = $cgi->path_info;
1105 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1107 $path =~ s/^\/(?:feed\/)?freshmeat\///og;
1109 my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
1110 my $flesh_feed = parse_feed_type($type);
1113 $limit = 10 if $limit !~ /^\d+$/;
1115 my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
1117 #if ($type eq 'opac') {
1118 # print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
1119 # join('&', map { "rl=" . $_ } @$list) .
1124 my $search = 'record';
1125 if ($rtype eq 'authority') {
1126 $search = 'authority';
1128 my $feed = create_record_feed( $search, $type, $list, $unapi, $org_unit->[0]->shortname, undef, $flesh_feed);
1132 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
1134 $feed->title("$limit most recent $rtype ${axis}s");
1137 $feed->creator($host);
1140 $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
1141 $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
1142 $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
1143 $feed->link(unapi => $unapi);
1147 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1148 join('&', map { 'rl=' . $_} @$list ),
1153 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1154 print $feed->toString . "\n";
1156 return Apache2::Const::OK;
1159 sub opensearch_osd {
1160 my $version = shift;
1165 if ($version eq '1.0') {
1167 Content-type: application/opensearchdescription+xml; charset=utf-8
1169 <?xml version="1.0" encoding="UTF-8"?>
1170 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
1171 <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&startPage={startPage}&startIndex={startIndex}&count={count}</Url>
1172 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
1173 <ShortName>$lib</ShortName>
1174 <LongName>Search $lib</LongName>
1175 <Description>Search the $lib OPAC by $class.</Description>
1176 <Tags>$lib book library</Tags>
1177 <SampleSearch>harry+potter</SampleSearch>
1178 <Developer>Mike Rylander for GPLS/PINES</Developer>
1179 <Contact>feedback\@open-ils.org</Contact>
1180 <SyndicationRight>open</SyndicationRight>
1181 <AdultContent>false</AdultContent>
1182 </OpenSearchDescription>
1186 Content-type: application/opensearchdescription+xml; charset=utf-8
1188 <?xml version="1.0" encoding="UTF-8"?>
1189 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
1190 <ShortName>$lib</ShortName>
1191 <Description>Search the $lib OPAC by $class.</Description>
1192 <Tags>$lib book library</Tags>
1193 <Url type="application/rss+xml"
1194 template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1195 <Url type="application/atom+xml"
1196 template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1197 <Url type="application/x-mods3+xml"
1198 template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1199 <Url type="application/x-mods+xml"
1200 template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1201 <Url type="application/octet-stream"
1202 template="$base/1.1/$lib/marc21/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1203 <Url type="application/x-marcxml+xml"
1204 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1205 <Url type="text/html"
1206 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1207 <LongName>Search $lib</LongName>
1208 <Query role="example" searchTerms="harry+potter" />
1209 <Developer>Mike Rylander for GPLS/PINES</Developer>
1210 <Contact>feedback\@open-ils.org</Contact>
1211 <SyndicationRight>open</SyndicationRight>
1212 <AdultContent>false</AdultContent>
1213 <Language>en-US</Language>
1214 <OutputEncoding>UTF-8</OutputEncoding>
1215 <InputEncoding>UTF-8</InputEncoding>
1216 </OpenSearchDescription>
1220 return Apache2::Const::OK;
1223 sub opensearch_feed {
1225 return Apache2::Const::DECLINED if (-e $apache->filename);
1230 my $year = (gmtime())[5] + 1900;
1232 my $host = $cgi->virtual_host || $cgi->server_name;
1235 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1236 my $rel_name = $cgi->url(-relative=>1);
1237 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1240 my $url = $cgi->url(-path_info=>$add_path);
1241 my $root = (split 'opensearch', $url)[0];
1242 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1243 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1245 my $path = $cgi->path_info;
1246 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1248 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1254 if (!$lib || $lib eq '-') {
1255 $lib = $actor->request(
1256 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1257 )->gather(1)->[0]->shortname;
1260 if ($class eq '-') {
1264 return opensearch_osd($version, $lib, $class, $base);
1268 my $page = $cgi->param('startPage') || 1;
1269 my $offset = $cgi->param('startIndex') || 1;
1270 my $limit = $cgi->param('count') || 10;
1272 $page = 1 if ($page !~ /^\d+$/);
1273 $offset = 1 if ($offset !~ /^\d+$/);
1274 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1277 $offset = ($page - 1) * $limit;
1282 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1283 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1285 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1286 $lang = '' if ($lang eq '*');
1288 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1290 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1293 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1294 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1296 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1299 $type = $cgi->param('responseType') if $cgi->param('responseType');
1302 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1306 my $kwt = $cgi->param('kw');
1307 my $tit = $cgi->param('ti');
1308 my $aut = $cgi->param('au');
1309 my $sut = $cgi->param('su');
1310 my $set = $cgi->param('se');
1312 $terms .= " " if ($terms && $kwt);
1313 $terms .= "keyword: $kwt" if ($kwt);
1314 $terms .= " " if ($terms && $tit);
1315 $terms .= "title: $tit" if ($tit);
1316 $terms .= " " if ($terms && $aut);
1317 $terms .= "author: $aut" if ($aut);
1318 $terms .= " " if ($terms && $sut);
1319 $terms .= "subject: $sut" if ($sut);
1320 $terms .= " " if ($terms && $set);
1321 $terms .= "series: $set" if ($set);
1323 if ($version eq '1.0') {
1325 } elsif ($type eq '-') {
1328 my $flesh_feed = parse_feed_type($type);
1330 $terms = decode_utf8($terms);
1331 $lang = 'eng' if ($lang eq 'en-US');
1333 $log->debug("OpenSearch terms: $terms");
1335 my $org_unit = get_ou($org);
1337 # Apostrophes break search and get indexed as spaces anyway
1338 my $safe_terms = $terms;
1339 $safe_terms =~ s{'}{ }go;
1341 my $recs = $search->request(
1342 'open-ils.search.biblio.multiclass.query' => {
1343 org_unit => $org_unit->[0]->id,
1347 sort_dir => $sortdir,
1348 default_class => $class,
1349 ($lang ? ( 'language' => $lang ) : ()),
1350 } => $safe_terms => 1
1353 $log->debug("Hits for [$terms]: $recs->{count}");
1355 my $feed = create_record_feed(
1358 [ map { $_->[0] } @{$recs->{ids}} ],
1365 $log->debug("Feed created...");
1369 $feed->search($safe_terms);
1370 $feed->class($class);
1372 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1374 $feed->creator($host);
1377 $feed->_create_node(
1378 $feed->{item_xpath},
1379 'http://a9.com/-/spec/opensearch/1.1/',
1384 $feed->_create_node(
1385 $feed->{item_xpath},
1386 'http://a9.com/-/spec/opensearch/1.1/',
1391 $feed->_create_node(
1392 $feed->{item_xpath},
1393 'http://a9.com/-/spec/opensearch/1.1/',
1398 $log->debug("...basic feed data added...");
1402 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1403 'application/opensearch+xml'
1404 ) if ($offset + $limit < $recs->{count});
1408 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1409 'application/opensearch+xml'
1414 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1415 'application/opensearch+xml'
1420 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1421 'application/rss+xml'
1426 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1427 'application/atom+xml'
1432 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1438 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1442 $feed->link( 'unapi-server' => $unapi);
1444 $log->debug("...feed links added...");
1448 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1449 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1453 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1454 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1456 $log->debug("...and feed returned.");
1458 return Apache2::Const::OK;
1461 sub create_record_feed {
1464 my $records = shift;
1467 my $lib = uc(shift()) || '-';
1474 my $base = $cgi->url;
1475 my $host = $cgi->virtual_host || $cgi->server_name;
1477 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
1481 my $tag_prefix = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d", $month, $day);
1483 my $flesh_feed = defined($flesh) ? $flesh : parse_feed_type($type);
1485 $type =~ s/(-full|-uris)$//o;
1487 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1488 $feed->base($base) if ($flesh);
1489 $feed->unapi($unapi) if ($flesh);
1491 $type = 'atom' if ($type eq 'html');
1492 $type = 'marcxml' if
1493 $type eq 'htmlholdings' or
1494 $type eq 'marctxt' or
1496 $type eq 'marc21'; # kludgy since it isn't an XML format, but needed
1498 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1501 for my $record (@$records) {
1502 next unless($record);
1504 #my $rec = $record->id;
1507 my $item_tag = "$tag_prefix:biblio-record_entry/$rec/$lib";
1508 $item_tag = "$tag_prefix:metabib-metarecord/$rec/$lib" if ($search eq 'metarecord');
1509 $item_tag = "$tag_prefix:isbn/$rec/$lib" if ($search eq 'isbn');
1510 $item_tag .= "/$depth" if (defined($depth));
1512 $item_tag = "$tag_prefix:authority-record_entry/$rec" if ($search eq 'authority');
1514 my $xml = $supercat->request(
1515 "open-ils.supercat.$search.$type.retrieve",
1520 my $node = $feed->add_item($xml);
1524 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && ($flesh > 0)) {
1525 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
1526 while ( !$r->complete ) {
1527 $xml .= join('', map {$_->content} $r->recv);
1529 $xml .= join('', map {$_->content} $r->recv);
1530 $node->add_holdings($xml);
1533 $node->id($item_tag);
1534 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1535 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
1536 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
1537 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1538 $node->link('unapi-id' => $item_tag) if ($flesh);
1546 return Apache2::Const::DECLINED if (-e $apache->filename);
1551 my $year = (gmtime())[5] + 1900;
1553 my $host = $cgi->virtual_host || $cgi->server_name;
1556 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1557 my $rel_name = $cgi->url(-relative=>1);
1558 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1561 my $url = $cgi->url(-path_info=>$add_path);
1562 my $root = (split 'browse', $url)[0];
1563 my $base = (split 'browse', $url)[0] . 'browse';
1564 my $unapi = (split 'browse', $url)[0] . 'unapi';
1566 my $path = $cgi->path_info;
1569 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1570 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1572 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1574 my $status = [$cgi->param('status')];
1575 my $cpLoc = [$cgi->param('copyLocation')];
1576 $site ||= $cgi->param('searchOrg');
1577 $page ||= $cgi->param('startPage') || 0;
1578 $page_size ||= $cgi->param('count') || 9;
1580 $page = 0 if ($page !~ /^-?\d+$/);
1581 $page_size = 9 if $page_size !~ /^\d+$/;
1583 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1584 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1586 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1587 warn "something's wrong...";
1588 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1592 $string = decode_utf8($string);
1593 $string =~ s/\+/ /go;
1597 if ($axis =~ /^authority/) {
1598 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1600 my $method = "open-ils.supercat.authority.browse_center.by_axis";
1601 $method .= ".refs" if $refs;
1603 $tree = $supercat->request(
1611 $tree = $supercat->request(
1612 "open-ils.supercat.$axis.browse",
1622 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1624 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1625 print $header.$content;
1626 return Apache2::Const::OK;
1629 sub string_startwith {
1631 return Apache2::Const::DECLINED if (-e $apache->filename);
1636 my $year = (gmtime())[5] + 1900;
1638 my $host = $cgi->virtual_host || $cgi->server_name;
1641 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1642 my $rel_name = $cgi->url(-relative=>1);
1643 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1646 my $url = $cgi->url(-path_info=>$add_path);
1647 my $root = (split 'startwith', $url)[0];
1648 my $base = (split 'startwith', $url)[0] . 'startwith';
1649 my $unapi = (split 'startwith', $url)[0] . 'unapi';
1651 my $path = $cgi->path_info;
1654 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1655 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1657 my $status = [$cgi->param('status')];
1658 my $cpLoc = [$cgi->param('copyLocation')];
1659 $site ||= $cgi->param('searchOrg');
1660 $page ||= $cgi->param('startPage') || 0;
1661 $page_size ||= $cgi->param('count') || 9;
1663 $page = 0 if ($page !~ /^-?\d+$/);
1664 $page_size = 9 if $page_size !~ /^\d+$/;
1666 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1667 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1669 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1670 warn "something's wrong...";
1671 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1675 $string = decode_utf8($string);
1676 $string =~ s/\+/ /go;
1680 if ($axis =~ /^authority/) {
1681 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1683 my $method = "open-ils.supercat.authority.browse_top.by_axis";
1684 $method .= ".refs" if $refs;
1686 $tree = $supercat->request(
1694 $tree = $supercat->request(
1695 "open-ils.supercat.$axis.startwith",
1705 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1707 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1708 print $header.$content;
1709 return Apache2::Const::OK;
1712 sub item_age_browse {
1714 return Apache2::Const::DECLINED if (-e $apache->filename);
1719 my $year = (gmtime())[5] + 1900;
1721 my $host = $cgi->virtual_host || $cgi->server_name;
1724 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1725 my $rel_name = $cgi->url(-relative=>1);
1726 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1729 my $url = $cgi->url(-path_info=>$add_path);
1730 my $root = (split 'browse', $url)[0];
1731 my $base = (split 'browse', $url)[0] . 'browse';
1732 my $unapi = (split 'browse', $url)[0] . 'unapi';
1734 my $path = $cgi->path_info;
1737 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1738 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1740 unless ($axis eq 'item-age') {
1741 warn "something's wrong...";
1742 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1746 my $status = [$cgi->param('status')];
1747 my $cpLoc = [$cgi->param('copyLocation')];
1748 $site ||= $cgi->param('searchOrg') || '-';
1749 $page ||= $cgi->param('startPage') || 1;
1750 $page_size ||= $cgi->param('count') || 10;
1752 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1753 $page_size = 10 if $page_size !~ /^\d+$/;
1755 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1756 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1758 my $recs = $supercat->request(
1759 "open-ils.supercat.new_book_list",
1767 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1769 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1770 print $header.$content;
1771 return Apache2::Const::OK;
1774 our %qualifier_ids = (
1775 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1776 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1777 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1781 # Our authority search options are currently pretty impoverished;
1782 # just right-truncated string match on a few categories, or by
1784 our %nested_auth_qualifier_map = (
1786 id => { index => 'id', title => 'Record number'},
1787 name => { index => 'author', title => 'Personal or corporate author, or meeting name'},
1788 title => { index => 'title', title => 'Uniform title'},
1789 subject => { index => 'subject', title => 'Chronological term, topical term, geographic name, or genre/form term'},
1790 topic => { index => 'topic', title => 'Topical term'},
1794 my $base_explain = <<XML;
1796 id="evergreen-sru-explain-full"
1797 authoritative="true"
1798 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1799 xmlns="http://explain.z3950.org/dtd/2.0/">
1800 <serverInfo transport="http" protocol="SRU" version="1.1">
1807 <title primary="true"/>
1808 <description primary="true"/>
1812 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1817 identifier="info:srw/schema/1/marcxml-v1.1"
1818 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1822 <title>MARC21Slim (marcxml)</title>
1827 <default type="numberOfRecords">10</default>
1828 <default type="contextSet">eg</default>
1829 <default type="index">keyword</default>
1830 <default type="relation">all</default>
1831 <default type="sortSchema">marcxml</default>
1832 <default type="retrieveSchema">marcxml</default>
1833 <setting type="maximumRecords">50</setting>
1834 <supports type="relationModifier">relevant</supports>
1835 <supports type="relationModifier">stem</supports>
1836 <supports type="relationModifier">fuzzy</supports>
1837 <supports type="relationModifier">word</supports>
1850 my $req = SRU::Request->newFromCGI( $cgi );
1851 my $resp = SRU::Response->newFromRequest( $req );
1853 # Find the org_unit shortname, if passed as part of the URL
1854 # http://example.com/opac/extras/sru/SHORTNAME
1855 my $url = $cgi->path_info;
1856 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1858 if ( $resp->type eq 'searchRetrieve' ) {
1860 # Older versions of Debian packages returned terms to us double-encoded,
1861 # so we had to forcefully double-decode them a second time with
1862 # an outer decode('utf8', $string) call; this seems to be resolved with
1863 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1864 my $cql_query = decode_utf8($req->query);
1865 my $search_string = decode_utf8($req->cql->toEvergreen);
1867 # Ensure the search string overrides the default site
1868 if ($shortname and $search_string !~ m#site:#) {
1869 $search_string .= " site:$shortname";
1872 my $offset = $req->startRecord;
1873 $offset-- if ($offset);
1876 my $limit = $req->maximumRecords;
1879 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1881 my $recs = $search->request(
1882 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1885 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1887 foreach my $record (@$bre) {
1888 my $marcxml = $record->marc;
1889 # Make the beast conform to a VDX-supported format
1890 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1891 # Trying to implement LIBSOL_852_A format; so much for standards
1893 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1894 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1896 # Force record leader to 'a' as our data is always UTF8
1897 # Avoids marc8_to_utf8 from being invoked with horrible results
1898 # on the off-chance the record leader isn't correct
1899 my $ldr = $marc->leader;
1900 substr($ldr, 9, 1, 'a');
1901 $marc->leader($ldr);
1903 # Expects the record ID in the 001
1904 $marc->delete_field($_) for ($marc->field('001'));
1905 if (!$marc->field('001')) {
1906 $marc->insert_fields_ordered(
1907 MARC::Field->new( '001', $record->id )
1910 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1911 foreach my $cn (keys %$bib_holdings) {
1912 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1913 $marc->insert_fields_ordered(
1916 a => $cp->{'location'},
1917 b => $bib_holdings->{$cn}->{'owning_lib'},
1919 d => $cp->{'circlib'},
1920 g => $cp->{'barcode'},
1921 n => $cp->{'status'},
1927 # Ensure the data is encoded as UTF8 before we hand it off
1928 $marcxml = encode_utf8($marc->as_xml_record());
1929 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1933 SRU::Response::Record->new(
1934 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1935 recordData => $marcxml,
1936 recordPosition => ++$offset
1941 $resp->numberOfRecords($recs->{count});
1943 } elsif ( $resp->type eq 'explain' ) {
1944 return_sru_explain($cgi, $req, $resp, \$ex_doc,
1946 \%OpenILS::WWW::SuperCat::qualifier_ids
1950 SRU::Response::Record->new(
1951 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1952 recordData => $ex_doc
1957 print $cgi->header( -type => 'application/xml' );
1958 print $U->entityize($resp->asXML) . "\n";
1959 return Apache2::Const::OK;
1964 package CQL::BooleanNode;
1968 my $left = $self->left();
1969 my $right = $self->right();
1970 my $leftStr = $left->toEvergreen;
1971 my $rightStr = $right->toEvergreen();
1973 my $op = '||' if uc $self->op() eq 'OR';
1976 return "$leftStr $rightStr";
1979 sub toEvergreenAuth {
1980 return toEvergreen(shift);
1983 package CQL::TermNode;
1987 my $qualifier = $self->getQualifier();
1988 my $term = $self->getTerm();
1989 my $relation = $self->getRelation();
1993 my ($qset, $qname) = split(/\./, $qualifier);
1995 # Per http://www.loc.gov/standards/sru/specs/cql.html
1996 # "All parts of CQL are case insensitive [...] If any case insensitive
1997 # part of CQL is specified with both upper and lower case, it is for
1998 # aesthetic purposes only."
2000 # So fold the qualifier and relation to lower case
2002 $qname = lc($qname);
2004 if ( exists($qualifier_map{$qset}{$qname}) ) {
2005 $qualifier = $qualifier_map{$qset}{$qname}{'index'} || 'kw';
2006 $log->debug("SRU toEvergreen: $qset, $qname $qualifier_map{$qset}{$qname}{'index'}\n");
2009 my @modifiers = $relation->getModifiers();
2011 my $base = $relation->getBase();
2012 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
2015 foreach my $m ( @modifiers ) {
2016 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
2022 $quote_it = 0 if ( $base eq 'all' );
2023 $term = maybeQuote($term) if $quote_it;
2026 croak( "Evergreen doesn't support the $base relations" );
2034 return "$qualifier:$term";
2037 sub toEvergreenAuth {
2039 my $qualifier = $self->getQualifier();
2040 my $term = $self->getTerm();
2041 my $relation = $self->getRelation();
2045 my ($qset, $qname) = split(/\./, $qualifier);
2047 if ( exists($OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}) ) {
2048 $qualifier = $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'} || 'author';
2049 $log->debug("SRU toEvergreenAuth: $qset, $qname $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'}\n");
2052 return { qualifier => $qualifier, term => $term };
2057 sub sru_auth_search {
2062 my $req = SRU::Request->newFromCGI( $cgi );
2063 my $resp = SRU::Response->newFromRequest( $req );
2065 if ( $resp->type eq 'searchRetrieve' ) {
2066 return_auth_response($cgi, $req, $resp);
2067 } elsif ( $resp->type eq 'explain' ) {
2068 return_sru_explain($cgi, $req, $resp, \$auth_ex_doc,
2069 \%OpenILS::WWW::SuperCat::nested_auth_qualifier_map,
2070 \%OpenILS::WWW::SuperCat::qualifier_ids
2074 print $cgi->header( -type => 'application/xml' );
2075 print $U->entityize($resp->asXML) . "\n";
2076 return Apache2::Const::OK;
2079 sub explain_header {
2082 my $host = $cgi->virtual_host || $cgi->server_name;
2085 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
2086 my $rel_name = $cgi->url(-relative=>1);
2087 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
2089 my $base = $cgi->url(-base=>1);
2090 my $url = $cgi->url(-path_info=>$add_path);
2091 $url =~ s/^$base\///o;
2093 my $doc = $parser->parse_string($base_explain);
2094 my $e = $doc->documentElement;
2095 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
2096 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
2097 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
2102 sub return_sru_explain {
2103 my ($cgi, $req, $resp, $explain, $index_map, $qualifier_ids) = @_;
2105 $index_map ||= \%qualifier_map;
2107 my ($doc, $e) = explain_header($cgi);
2108 for my $name ( keys %{$index_map} ) {
2110 my $identifier = $qualifier_ids->{ $name };
2112 next unless $identifier;
2114 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
2115 $set_node->setAttribute( identifier => $identifier );
2116 $set_node->setAttribute( name => $name );
2118 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
2119 for my $index ( sort keys %{$index_map->{$name}} ) {
2120 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
2122 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
2123 $map_node->appendChild( $name_node );
2125 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
2127 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
2128 $index_node->appendChild( $title_node );
2129 $index_node->appendChild( $map_node );
2131 $index_node->setAttribute( id => "$name.$index" );
2132 $title_node->appendText($index_map->{$name}{$index}{'title'});
2133 $name_node->setAttribute( set => $name );
2134 $name_node->appendText($index_map->{$name}{$index}{'index'});
2136 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
2140 $$explain = $e->toString;
2144 SRU::Response::Record->new(
2145 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
2146 recordData => $$explain
2152 sub return_auth_response {
2153 my ($cgi, $req, $resp) = @_;
2155 my $cql_query = decode_utf8($req->query);
2156 my $search = $req->cql->toEvergreenAuth;
2158 my $qualifier = decode_utf8($search->{qualifier});
2159 my $term = decode_utf8($search->{term});
2161 $log->info("SRU NAF search string [$cql_query] converted to "
2162 . "[$qualifier:$term]\n");
2164 my $page_size = $req->maximumRecords;
2167 # startwith deals with pages, so convert startRecord to a page number
2168 my $page = ($req->startRecord / $page_size) || 0;
2171 if ($qualifier eq "id") {
2172 $recs = [ int($term) ];
2174 my ($realaxis, $refs) = ($qualifier =~ $authority_axis_re);
2176 my $method = "open-ils.supercat.authority.browse_top.by_axis";
2177 $method .= ".refs" if $refs;
2179 $recs = $supercat->request(
2188 my $record_position = $req->startRecord;
2189 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2190 foreach my $record (@$recs) {
2191 my $marcxml = $cstore->request(
2192 'open-ils.cstore.direct.authority.record_entry.retrieve', $record
2196 SRU::Response::Record->new(
2197 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
2198 recordData => $marcxml,
2199 recordPosition => ++$record_position
2204 $resp->numberOfRecords(scalar(@$recs));
2207 =head2 get_ou($org_unit)
2209 Returns an aou object for a given actor.org_unit shortname or ID.
2214 my $org = shift || '-';
2218 $org_unit = $actor->request(
2219 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2221 } elsif ($org !~ /^\d+$/o) {
2222 $org_unit = $actor->request(
2223 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2226 $org_unit = $actor->request(
2227 'open-ils.actor.org_unit_list.search' => id => $org