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);
1717 my $year = (gmtime())[5] + 1900;
1719 my $host = $cgi->virtual_host || $cgi->server_name;
1722 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1723 my $rel_name = $cgi->url(-relative=>1);
1724 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1727 my $url = $cgi->url(-path_info=>$add_path);
1728 my $root = (split 'browse', $url)[0];
1729 my $base = (split 'browse', $url)[0] . 'browse';
1730 my $unapi = (split 'browse', $url)[0] . 'unapi';
1732 my $path = $cgi->path_info;
1735 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1736 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1738 unless ($axis eq 'item-age') {
1739 warn "something's wrong...";
1740 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1744 my $status = [$cgi->param('status')];
1745 my $cpLoc = [$cgi->param('copyLocation')];
1746 $site ||= $cgi->param('searchOrg') || '-';
1747 $page ||= $cgi->param('startPage') || 1;
1748 $page_size ||= $cgi->param('count') || 10;
1750 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1751 $page_size = 10 if $page_size !~ /^\d+$/;
1753 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1754 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1756 my $recs = $supercat->request(
1757 "open-ils.supercat.new_book_list",
1765 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1767 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1768 print $header.$content;
1769 return Apache2::Const::OK;
1772 our %qualifier_ids = (
1773 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1774 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1775 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1779 # Our authority search options are currently pretty impoverished;
1780 # just right-truncated string match on a few categories, or by
1782 our %nested_auth_qualifier_map = (
1784 id => { index => 'id', title => 'Record number'},
1785 name => { index => 'author', title => 'Personal or corporate author, or meeting name'},
1786 title => { index => 'title', title => 'Uniform title'},
1787 subject => { index => 'subject', title => 'Chronological term, topical term, geographic name, or genre/form term'},
1788 topic => { index => 'topic', title => 'Topical term'},
1792 my $base_explain = <<XML;
1794 id="evergreen-sru-explain-full"
1795 authoritative="true"
1796 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1797 xmlns="http://explain.z3950.org/dtd/2.0/">
1798 <serverInfo transport="http" protocol="SRU" version="1.1">
1805 <title primary="true"/>
1806 <description primary="true"/>
1810 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1815 identifier="info:srw/schema/1/marcxml-v1.1"
1816 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1820 <title>MARC21Slim (marcxml)</title>
1825 <default type="numberOfRecords">10</default>
1826 <default type="contextSet">eg</default>
1827 <default type="index">keyword</default>
1828 <default type="relation">all</default>
1829 <default type="sortSchema">marcxml</default>
1830 <default type="retrieveSchema">marcxml</default>
1831 <setting type="maximumRecords">50</setting>
1832 <supports type="relationModifier">relevant</supports>
1833 <supports type="relationModifier">stem</supports>
1834 <supports type="relationModifier">fuzzy</supports>
1835 <supports type="relationModifier">word</supports>
1848 my $req = SRU::Request->newFromCGI( $cgi );
1849 my $resp = SRU::Response->newFromRequest( $req );
1851 # Find the org_unit shortname, if passed as part of the URL
1852 # http://example.com/opac/extras/sru/SHORTNAME
1853 my $url = $cgi->path_info;
1854 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1856 if ( $resp->type eq 'searchRetrieve' ) {
1858 # Older versions of Debian packages returned terms to us double-encoded,
1859 # so we had to forcefully double-decode them a second time with
1860 # an outer decode('utf8', $string) call; this seems to be resolved with
1861 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1862 my $cql_query = decode_utf8($req->query);
1863 my $search_string = decode_utf8($req->cql->toEvergreen);
1865 # Ensure the search string overrides the default site
1866 if ($shortname and $search_string !~ m#site:#) {
1867 $search_string .= " site:$shortname";
1870 my $offset = $req->startRecord;
1871 $offset-- if ($offset);
1874 my $limit = $req->maximumRecords;
1877 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1879 my $recs = $search->request(
1880 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1883 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1885 foreach my $record (@$bre) {
1886 my $marcxml = $record->marc;
1887 # Make the beast conform to a VDX-supported format
1888 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1889 # Trying to implement LIBSOL_852_A format; so much for standards
1891 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1892 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1894 # Force record leader to 'a' as our data is always UTF8
1895 # Avoids marc8_to_utf8 from being invoked with horrible results
1896 # on the off-chance the record leader isn't correct
1897 my $ldr = $marc->leader;
1898 substr($ldr, 9, 1, 'a');
1899 $marc->leader($ldr);
1901 # Expects the record ID in the 001
1902 $marc->delete_field($_) for ($marc->field('001'));
1903 if (!$marc->field('001')) {
1904 $marc->insert_fields_ordered(
1905 MARC::Field->new( '001', $record->id )
1908 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1909 foreach my $cn (keys %$bib_holdings) {
1910 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1911 $marc->insert_fields_ordered(
1914 a => $cp->{'location'},
1915 b => $bib_holdings->{$cn}->{'owning_lib'},
1917 d => $cp->{'circlib'},
1918 g => $cp->{'barcode'},
1919 n => $cp->{'status'},
1925 # Ensure the data is encoded as UTF8 before we hand it off
1926 $marcxml = encode_utf8($marc->as_xml_record());
1927 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1931 SRU::Response::Record->new(
1932 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1933 recordData => $marcxml,
1934 recordPosition => ++$offset
1939 $resp->numberOfRecords($recs->{count});
1941 } elsif ( $resp->type eq 'explain' ) {
1942 return_sru_explain($cgi, $req, $resp, \$ex_doc,
1944 \%OpenILS::WWW::SuperCat::qualifier_ids
1948 SRU::Response::Record->new(
1949 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1950 recordData => $ex_doc
1955 print $cgi->header( -type => 'application/xml' );
1956 print $U->entityize($resp->asXML) . "\n";
1957 return Apache2::Const::OK;
1962 package CQL::BooleanNode;
1966 my $left = $self->left();
1967 my $right = $self->right();
1968 my $leftStr = $left->toEvergreen;
1969 my $rightStr = $right->toEvergreen();
1971 my $op = '||' if uc $self->op() eq 'OR';
1974 return "$leftStr $rightStr";
1977 sub toEvergreenAuth {
1978 return toEvergreen(shift);
1981 package CQL::TermNode;
1985 my $qualifier = $self->getQualifier();
1986 my $term = $self->getTerm();
1987 my $relation = $self->getRelation();
1991 my ($qset, $qname) = split(/\./, $qualifier);
1993 # Per http://www.loc.gov/standards/sru/specs/cql.html
1994 # "All parts of CQL are case insensitive [...] If any case insensitive
1995 # part of CQL is specified with both upper and lower case, it is for
1996 # aesthetic purposes only."
1998 # So fold the qualifier and relation to lower case
2000 $qname = lc($qname);
2002 if ( exists($qualifier_map{$qset}{$qname}) ) {
2003 $qualifier = $qualifier_map{$qset}{$qname}{'index'} || 'kw';
2004 $log->debug("SRU toEvergreen: $qset, $qname $qualifier_map{$qset}{$qname}{'index'}\n");
2007 my @modifiers = $relation->getModifiers();
2009 my $base = $relation->getBase();
2010 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
2013 foreach my $m ( @modifiers ) {
2014 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
2020 $quote_it = 0 if ( $base eq 'all' );
2021 $term = maybeQuote($term) if $quote_it;
2024 croak( "Evergreen doesn't support the $base relations" );
2032 return "$qualifier:$term";
2035 sub toEvergreenAuth {
2037 my $qualifier = $self->getQualifier();
2038 my $term = $self->getTerm();
2039 my $relation = $self->getRelation();
2043 my ($qset, $qname) = split(/\./, $qualifier);
2045 if ( exists($OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}) ) {
2046 $qualifier = $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'} || 'author';
2047 $log->debug("SRU toEvergreenAuth: $qset, $qname $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'}\n");
2050 return { qualifier => $qualifier, term => $term };
2055 sub sru_auth_search {
2060 my $req = SRU::Request->newFromCGI( $cgi );
2061 my $resp = SRU::Response->newFromRequest( $req );
2063 if ( $resp->type eq 'searchRetrieve' ) {
2064 return_auth_response($cgi, $req, $resp);
2065 } elsif ( $resp->type eq 'explain' ) {
2066 return_sru_explain($cgi, $req, $resp, \$auth_ex_doc,
2067 \%OpenILS::WWW::SuperCat::nested_auth_qualifier_map,
2068 \%OpenILS::WWW::SuperCat::qualifier_ids
2072 print $cgi->header( -type => 'application/xml' );
2073 print $U->entityize($resp->asXML) . "\n";
2074 return Apache2::Const::OK;
2077 sub explain_header {
2080 my $host = $cgi->virtual_host || $cgi->server_name;
2083 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
2084 my $rel_name = $cgi->url(-relative=>1);
2085 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
2087 my $base = $cgi->url(-base=>1);
2088 my $url = $cgi->url(-path_info=>$add_path);
2089 $url =~ s/^$base\///o;
2091 my $doc = $parser->parse_string($base_explain);
2092 my $e = $doc->documentElement;
2093 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
2094 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
2095 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
2100 sub return_sru_explain {
2101 my ($cgi, $req, $resp, $explain, $index_map, $qualifier_ids) = @_;
2103 $index_map ||= \%qualifier_map;
2105 my ($doc, $e) = explain_header($cgi);
2106 for my $name ( keys %{$index_map} ) {
2108 my $identifier = $qualifier_ids->{ $name };
2110 next unless $identifier;
2112 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
2113 $set_node->setAttribute( identifier => $identifier );
2114 $set_node->setAttribute( name => $name );
2116 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
2117 for my $index ( sort keys %{$index_map->{$name}} ) {
2118 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
2120 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
2121 $map_node->appendChild( $name_node );
2123 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
2125 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
2126 $index_node->appendChild( $title_node );
2127 $index_node->appendChild( $map_node );
2129 $index_node->setAttribute( id => "$name.$index" );
2130 $title_node->appendText($index_map->{$name}{$index}{'title'});
2131 $name_node->setAttribute( set => $name );
2132 $name_node->appendText($index_map->{$name}{$index}{'index'});
2134 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
2138 $$explain = $e->toString;
2142 SRU::Response::Record->new(
2143 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
2144 recordData => $$explain
2150 sub return_auth_response {
2151 my ($cgi, $req, $resp) = @_;
2153 my $cql_query = decode_utf8($req->query);
2154 my $search = $req->cql->toEvergreenAuth;
2156 my $qualifier = decode_utf8($search->{qualifier});
2157 my $term = decode_utf8($search->{term});
2159 $log->info("SRU NAF search string [$cql_query] converted to "
2160 . "[$qualifier:$term]\n");
2162 my $page_size = $req->maximumRecords;
2165 # startwith deals with pages, so convert startRecord to a page number
2166 my $page = ($req->startRecord / $page_size) || 0;
2169 if ($qualifier eq "id") {
2170 $recs = [ int($term) ];
2172 my ($realaxis, $refs) = ($qualifier =~ $authority_axis_re);
2174 my $method = "open-ils.supercat.authority.browse_top.by_axis";
2175 $method .= ".refs" if $refs;
2177 $recs = $supercat->request(
2186 my $record_position = $req->startRecord;
2187 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2188 foreach my $record (@$recs) {
2189 my $marcxml = $cstore->request(
2190 'open-ils.cstore.direct.authority.record_entry.retrieve', $record
2194 SRU::Response::Record->new(
2195 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
2196 recordData => $marcxml,
2197 recordPosition => ++$record_position
2202 $resp->numberOfRecords(scalar(@$recs));
2205 =head2 get_ou($org_unit)
2207 Returns an aou object for a given actor.org_unit shortname or ID.
2212 my $org = shift || '-';
2216 $org_unit = $actor->request(
2217 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2219 } elsif ($org !~ /^\d+$/o) {
2220 $org_unit = $actor->request(
2221 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2224 $org_unit = $actor->request(
2225 'open-ils.actor.org_unit_list.search' => id => $org