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 $U->entityize($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 $U->entityize($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 $U->entityize($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 $U->entityize($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/x-marcxml+xml"
1202 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1203 <Url type="text/html"
1204 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1205 <LongName>Search $lib</LongName>
1206 <Query role="example" searchTerms="harry+potter" />
1207 <Developer>Mike Rylander for GPLS/PINES</Developer>
1208 <Contact>feedback\@open-ils.org</Contact>
1209 <SyndicationRight>open</SyndicationRight>
1210 <AdultContent>false</AdultContent>
1211 <Language>en-US</Language>
1212 <OutputEncoding>UTF-8</OutputEncoding>
1213 <InputEncoding>UTF-8</InputEncoding>
1214 </OpenSearchDescription>
1218 return Apache2::Const::OK;
1221 sub opensearch_feed {
1223 return Apache2::Const::DECLINED if (-e $apache->filename);
1228 my $year = (gmtime())[5] + 1900;
1230 my $host = $cgi->virtual_host || $cgi->server_name;
1233 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1234 my $rel_name = $cgi->url(-relative=>1);
1235 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1238 my $url = $cgi->url(-path_info=>$add_path);
1239 my $root = (split 'opensearch', $url)[0];
1240 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1241 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1243 my $path = $cgi->path_info;
1244 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1246 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1252 if (!$lib || $lib eq '-') {
1253 $lib = $actor->request(
1254 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1255 )->gather(1)->[0]->shortname;
1258 if ($class eq '-') {
1262 return opensearch_osd($version, $lib, $class, $base);
1266 my $page = $cgi->param('startPage') || 1;
1267 my $offset = $cgi->param('startIndex') || 1;
1268 my $limit = $cgi->param('count') || 10;
1270 $page = 1 if ($page !~ /^\d+$/);
1271 $offset = 1 if ($offset !~ /^\d+$/);
1272 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1275 $offset = ($page - 1) * $limit;
1280 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1281 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1283 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1284 $lang = '' if ($lang eq '*');
1286 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1288 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1291 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1292 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1294 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1297 $type = $cgi->param('responseType') if $cgi->param('responseType');
1300 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1304 my $kwt = $cgi->param('kw');
1305 my $tit = $cgi->param('ti');
1306 my $aut = $cgi->param('au');
1307 my $sut = $cgi->param('su');
1308 my $set = $cgi->param('se');
1310 $terms .= " " if ($terms && $kwt);
1311 $terms .= "keyword: $kwt" if ($kwt);
1312 $terms .= " " if ($terms && $tit);
1313 $terms .= "title: $tit" if ($tit);
1314 $terms .= " " if ($terms && $aut);
1315 $terms .= "author: $aut" if ($aut);
1316 $terms .= " " if ($terms && $sut);
1317 $terms .= "subject: $sut" if ($sut);
1318 $terms .= " " if ($terms && $set);
1319 $terms .= "series: $set" if ($set);
1321 if ($version eq '1.0') {
1323 } elsif ($type eq '-') {
1326 my $flesh_feed = parse_feed_type($type);
1328 $terms = decode_utf8($terms);
1329 $lang = 'eng' if ($lang eq 'en-US');
1331 $log->debug("OpenSearch terms: $terms");
1333 my $org_unit = get_ou($org);
1335 # Apostrophes break search and get indexed as spaces anyway
1336 my $safe_terms = $terms;
1337 $safe_terms =~ s{'}{ }go;
1339 my $recs = $search->request(
1340 'open-ils.search.biblio.multiclass.query' => {
1341 org_unit => $org_unit->[0]->id,
1345 sort_dir => $sortdir,
1346 default_class => $class,
1347 ($lang ? ( 'language' => $lang ) : ()),
1348 } => $safe_terms => 1
1351 $log->debug("Hits for [$terms]: $recs->{count}");
1353 my $feed = create_record_feed(
1356 [ map { $_->[0] } @{$recs->{ids}} ],
1363 $log->debug("Feed created...");
1367 $feed->search($safe_terms);
1368 $feed->class($class);
1370 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1372 $feed->creator($host);
1375 $feed->_create_node(
1376 $feed->{item_xpath},
1377 'http://a9.com/-/spec/opensearch/1.1/',
1382 $feed->_create_node(
1383 $feed->{item_xpath},
1384 'http://a9.com/-/spec/opensearch/1.1/',
1389 $feed->_create_node(
1390 $feed->{item_xpath},
1391 'http://a9.com/-/spec/opensearch/1.1/',
1396 $log->debug("...basic feed data added...");
1400 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1401 'application/opensearch+xml'
1402 ) if ($offset + $limit < $recs->{count});
1406 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1407 'application/opensearch+xml'
1412 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1413 'application/opensearch+xml'
1418 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1419 'application/rss+xml'
1424 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1425 'application/atom+xml'
1430 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1436 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1440 $feed->link( 'unapi-server' => $unapi);
1442 $log->debug("...feed links added...");
1446 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1447 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1451 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1452 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1454 $log->debug("...and feed returned.");
1456 return Apache2::Const::OK;
1459 sub create_record_feed {
1462 my $records = shift;
1465 my $lib = uc(shift()) || '-';
1472 my $base = $cgi->url;
1473 my $host = $cgi->virtual_host || $cgi->server_name;
1475 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
1479 my $tag_prefix = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d", $month, $day);
1481 my $flesh_feed = defined($flesh) ? $flesh : parse_feed_type($type);
1483 $type =~ s/(-full|-uris)$//o;
1485 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1486 $feed->base($base) if ($flesh);
1487 $feed->unapi($unapi) if ($flesh);
1489 $type = 'atom' if ($type eq 'html');
1490 $type = 'marcxml' if (($type eq 'htmlholdings') || ($type eq 'marctxt') || ($type eq 'ris'));
1492 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1495 for my $record (@$records) {
1496 next unless($record);
1498 #my $rec = $record->id;
1501 my $item_tag = "$tag_prefix:biblio-record_entry/$rec/$lib";
1502 $item_tag = "$tag_prefix:metabib-metarecord/$rec/$lib" if ($search eq 'metarecord');
1503 $item_tag = "$tag_prefix:isbn/$rec/$lib" if ($search eq 'isbn');
1504 $item_tag .= "/$depth" if (defined($depth));
1506 $item_tag = "$tag_prefix:authority-record_entry/$rec" if ($search eq 'authority');
1508 my $xml = $supercat->request(
1509 "open-ils.supercat.$search.$type.retrieve",
1514 my $node = $feed->add_item($xml);
1518 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && ($flesh > 0)) {
1519 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
1520 while ( !$r->complete ) {
1521 $xml .= join('', map {$_->content} $r->recv);
1523 $xml .= join('', map {$_->content} $r->recv);
1524 $node->add_holdings($xml);
1527 $node->id($item_tag);
1528 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1529 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
1530 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
1531 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1532 $node->link('unapi-id' => $item_tag) if ($flesh);
1540 return Apache2::Const::DECLINED if (-e $apache->filename);
1545 my $year = (gmtime())[5] + 1900;
1547 my $host = $cgi->virtual_host || $cgi->server_name;
1550 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1551 my $rel_name = $cgi->url(-relative=>1);
1552 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1555 my $url = $cgi->url(-path_info=>$add_path);
1556 my $root = (split 'browse', $url)[0];
1557 my $base = (split 'browse', $url)[0] . 'browse';
1558 my $unapi = (split 'browse', $url)[0] . 'unapi';
1560 my $path = $cgi->path_info;
1563 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1564 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1566 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1568 my $status = [$cgi->param('status')];
1569 my $cpLoc = [$cgi->param('copyLocation')];
1570 $site ||= $cgi->param('searchOrg');
1571 $page ||= $cgi->param('startPage') || 0;
1572 $page_size ||= $cgi->param('count') || 9;
1574 $page = 0 if ($page !~ /^-?\d+$/);
1575 $page_size = 9 if $page_size !~ /^\d+$/;
1577 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1578 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1580 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1581 warn "something's wrong...";
1582 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1586 $string = decode_utf8($string);
1587 $string =~ s/\+/ /go;
1591 if ($axis =~ /^authority/) {
1592 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1594 my $method = "open-ils.supercat.authority.browse_center.by_axis";
1595 $method .= ".refs" if $refs;
1597 $tree = $supercat->request(
1605 $tree = $supercat->request(
1606 "open-ils.supercat.$axis.browse",
1616 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1618 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1619 print $header.$content;
1620 return Apache2::Const::OK;
1623 sub string_startwith {
1625 return Apache2::Const::DECLINED if (-e $apache->filename);
1630 my $year = (gmtime())[5] + 1900;
1632 my $host = $cgi->virtual_host || $cgi->server_name;
1635 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1636 my $rel_name = $cgi->url(-relative=>1);
1637 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1640 my $url = $cgi->url(-path_info=>$add_path);
1641 my $root = (split 'startwith', $url)[0];
1642 my $base = (split 'startwith', $url)[0] . 'startwith';
1643 my $unapi = (split 'startwith', $url)[0] . 'unapi';
1645 my $path = $cgi->path_info;
1648 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1649 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1651 my $status = [$cgi->param('status')];
1652 my $cpLoc = [$cgi->param('copyLocation')];
1653 $site ||= $cgi->param('searchOrg');
1654 $page ||= $cgi->param('startPage') || 0;
1655 $page_size ||= $cgi->param('count') || 9;
1657 $page = 0 if ($page !~ /^-?\d+$/);
1658 $page_size = 9 if $page_size !~ /^\d+$/;
1660 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1661 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1663 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1664 warn "something's wrong...";
1665 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1669 $string = decode_utf8($string);
1670 $string =~ s/\+/ /go;
1674 if ($axis =~ /^authority/) {
1675 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1677 my $method = "open-ils.supercat.authority.browse_top.by_axis";
1678 $method .= ".refs" if $refs;
1680 $tree = $supercat->request(
1688 $tree = $supercat->request(
1689 "open-ils.supercat.$axis.startwith",
1699 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1701 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1702 print $header.$content;
1703 return Apache2::Const::OK;
1706 sub item_age_browse {
1708 return Apache2::Const::DECLINED if (-e $apache->filename);
1711 my $year = (gmtime())[5] + 1900;
1713 my $host = $cgi->virtual_host || $cgi->server_name;
1716 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1717 my $rel_name = $cgi->url(-relative=>1);
1718 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1721 my $url = $cgi->url(-path_info=>$add_path);
1722 my $root = (split 'browse', $url)[0];
1723 my $base = (split 'browse', $url)[0] . 'browse';
1724 my $unapi = (split 'browse', $url)[0] . 'unapi';
1726 my $path = $cgi->path_info;
1729 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1730 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1732 unless ($axis eq 'item-age') {
1733 warn "something's wrong...";
1734 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1738 my $status = [$cgi->param('status')];
1739 my $cpLoc = [$cgi->param('copyLocation')];
1740 $site ||= $cgi->param('searchOrg') || '-';
1741 $page ||= $cgi->param('startPage') || 1;
1742 $page_size ||= $cgi->param('count') || 10;
1744 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1745 $page_size = 10 if $page_size !~ /^\d+$/;
1747 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1748 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1750 my $recs = $supercat->request(
1751 "open-ils.supercat.new_book_list",
1759 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1761 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1762 print $header.$content;
1763 return Apache2::Const::OK;
1766 our %qualifier_ids = (
1767 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1768 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1769 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1773 # Our authority search options are currently pretty impoverished;
1774 # just right-truncated string match on a few categories, or by
1776 our %nested_auth_qualifier_map = (
1778 id => { index => 'id', title => 'Record number'},
1779 name => { index => 'author', title => 'Personal or corporate author, or meeting name'},
1780 title => { index => 'title', title => 'Uniform title'},
1781 subject => { index => 'subject', title => 'Chronological term, topical term, geographic name, or genre/form term'},
1782 topic => { index => 'topic', title => 'Topical term'},
1786 my $base_explain = <<XML;
1788 id="evergreen-sru-explain-full"
1789 authoritative="true"
1790 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1791 xmlns="http://explain.z3950.org/dtd/2.0/">
1792 <serverInfo transport="http" protocol="SRU" version="1.1">
1799 <title primary="true"/>
1800 <description primary="true"/>
1804 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1809 identifier="info:srw/schema/1/marcxml-v1.1"
1810 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1814 <title>MARC21Slim (marcxml)</title>
1819 <default type="numberOfRecords">10</default>
1820 <default type="contextSet">eg</default>
1821 <default type="index">keyword</default>
1822 <default type="relation">all</default>
1823 <default type="sortSchema">marcxml</default>
1824 <default type="retrieveSchema">marcxml</default>
1825 <setting type="maximumRecords">50</setting>
1826 <supports type="relationModifier">relevant</supports>
1827 <supports type="relationModifier">stem</supports>
1828 <supports type="relationModifier">fuzzy</supports>
1829 <supports type="relationModifier">word</supports>
1842 my $req = SRU::Request->newFromCGI( $cgi );
1843 my $resp = SRU::Response->newFromRequest( $req );
1845 # Find the org_unit shortname, if passed as part of the URL
1846 # http://example.com/opac/extras/sru/SHORTNAME
1847 my $url = $cgi->path_info;
1848 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1850 if ( $resp->type eq 'searchRetrieve' ) {
1852 # Older versions of Debian packages returned terms to us double-encoded,
1853 # so we had to forcefully double-decode them a second time with
1854 # an outer decode('utf8', $string) call; this seems to be resolved with
1855 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1856 my $cql_query = decode_utf8($req->query);
1857 my $search_string = decode_utf8($req->cql->toEvergreen);
1859 # Ensure the search string overrides the default site
1860 if ($shortname and $search_string !~ m#site:#) {
1861 $search_string .= " site:$shortname";
1864 my $offset = $req->startRecord;
1865 $offset-- if ($offset);
1868 my $limit = $req->maximumRecords;
1871 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1873 my $recs = $search->request(
1874 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1877 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1879 foreach my $record (@$bre) {
1880 my $marcxml = $record->marc;
1881 # Make the beast conform to a VDX-supported format
1882 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1883 # Trying to implement LIBSOL_852_A format; so much for standards
1885 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1886 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1888 # Force record leader to 'a' as our data is always UTF8
1889 # Avoids marc8_to_utf8 from being invoked with horrible results
1890 # on the off-chance the record leader isn't correct
1891 my $ldr = $marc->leader;
1892 substr($ldr, 9, 1, 'a');
1893 $marc->leader($ldr);
1895 # Expects the record ID in the 001
1896 $marc->delete_field($_) for ($marc->field('001'));
1897 if (!$marc->field('001')) {
1898 $marc->insert_fields_ordered(
1899 MARC::Field->new( '001', $record->id )
1902 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1903 foreach my $cn (keys %$bib_holdings) {
1904 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1905 $marc->insert_fields_ordered(
1908 a => $cp->{'location'},
1909 b => $bib_holdings->{$cn}->{'owning_lib'},
1911 d => $cp->{'circlib'},
1912 g => $cp->{'barcode'},
1913 n => $cp->{'status'},
1919 # Ensure the data is encoded as UTF8 before we hand it off
1920 $marcxml = encode_utf8($marc->as_xml_record());
1921 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1925 SRU::Response::Record->new(
1926 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1927 recordData => $marcxml,
1928 recordPosition => ++$offset
1933 $resp->numberOfRecords($recs->{count});
1935 } elsif ( $resp->type eq 'explain' ) {
1936 return_sru_explain($cgi, $req, $resp, \$ex_doc,
1938 \%OpenILS::WWW::SuperCat::qualifier_ids
1942 SRU::Response::Record->new(
1943 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1944 recordData => $ex_doc
1949 print $cgi->header( -type => 'application/xml' );
1950 print $U->entityize($resp->asXML) . "\n";
1951 return Apache2::Const::OK;
1956 package CQL::BooleanNode;
1960 my $left = $self->left();
1961 my $right = $self->right();
1962 my $leftStr = $left->toEvergreen;
1963 my $rightStr = $right->toEvergreen();
1965 my $op = '||' if uc $self->op() eq 'OR';
1968 return "$leftStr $rightStr";
1971 sub toEvergreenAuth {
1972 return toEvergreen(shift);
1975 package CQL::TermNode;
1979 my $qualifier = $self->getQualifier();
1980 my $term = $self->getTerm();
1981 my $relation = $self->getRelation();
1985 my ($qset, $qname) = split(/\./, $qualifier);
1987 # Per http://www.loc.gov/standards/sru/specs/cql.html
1988 # "All parts of CQL are case insensitive [...] If any case insensitive
1989 # part of CQL is specified with both upper and lower case, it is for
1990 # aesthetic purposes only."
1992 # So fold the qualifier and relation to lower case
1994 $qname = lc($qname);
1996 if ( exists($qualifier_map{$qset}{$qname}) ) {
1997 $qualifier = $qualifier_map{$qset}{$qname}{'index'} || 'kw';
1998 $log->debug("SRU toEvergreen: $qset, $qname $qualifier_map{$qset}{$qname}{'index'}\n");
2001 my @modifiers = $relation->getModifiers();
2003 my $base = $relation->getBase();
2004 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
2007 foreach my $m ( @modifiers ) {
2008 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
2014 $quote_it = 0 if ( $base eq 'all' );
2015 $term = maybeQuote($term) if $quote_it;
2018 croak( "Evergreen doesn't support the $base relations" );
2026 return "$qualifier:$term";
2029 sub toEvergreenAuth {
2031 my $qualifier = $self->getQualifier();
2032 my $term = $self->getTerm();
2033 my $relation = $self->getRelation();
2037 my ($qset, $qname) = split(/\./, $qualifier);
2039 if ( exists($OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}) ) {
2040 $qualifier = $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'} || 'author';
2041 $log->debug("SRU toEvergreenAuth: $qset, $qname $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'}\n");
2044 return { qualifier => $qualifier, term => $term };
2049 sub sru_auth_search {
2054 my $req = SRU::Request->newFromCGI( $cgi );
2055 my $resp = SRU::Response->newFromRequest( $req );
2057 if ( $resp->type eq 'searchRetrieve' ) {
2058 return_auth_response($cgi, $req, $resp);
2059 } elsif ( $resp->type eq 'explain' ) {
2060 return_sru_explain($cgi, $req, $resp, \$auth_ex_doc,
2061 \%OpenILS::WWW::SuperCat::nested_auth_qualifier_map,
2062 \%OpenILS::WWW::SuperCat::qualifier_ids
2066 print $cgi->header( -type => 'application/xml' );
2067 print $U->entityize($resp->asXML) . "\n";
2068 return Apache2::Const::OK;
2071 sub explain_header {
2074 my $host = $cgi->virtual_host || $cgi->server_name;
2077 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
2078 my $rel_name = $cgi->url(-relative=>1);
2079 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
2081 my $base = $cgi->url(-base=>1);
2082 my $url = $cgi->url(-path_info=>$add_path);
2083 $url =~ s/^$base\///o;
2085 my $doc = $parser->parse_string($base_explain);
2086 my $e = $doc->documentElement;
2087 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
2088 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
2089 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
2094 sub return_sru_explain {
2095 my ($cgi, $req, $resp, $explain, $index_map, $qualifier_ids) = @_;
2097 $index_map ||= \%qualifier_map;
2099 my ($doc, $e) = explain_header($cgi);
2100 for my $name ( keys %{$index_map} ) {
2102 my $identifier = $qualifier_ids->{ $name };
2104 next unless $identifier;
2106 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
2107 $set_node->setAttribute( identifier => $identifier );
2108 $set_node->setAttribute( name => $name );
2110 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
2111 for my $index ( sort keys %{$index_map->{$name}} ) {
2112 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
2114 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
2115 $map_node->appendChild( $name_node );
2117 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
2119 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
2120 $index_node->appendChild( $title_node );
2121 $index_node->appendChild( $map_node );
2123 $index_node->setAttribute( id => "$name.$index" );
2124 $title_node->appendText($index_map->{$name}{$index}{'title'});
2125 $name_node->setAttribute( set => $name );
2126 $name_node->appendText($index_map->{$name}{$index}{'index'});
2128 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
2132 $$explain = $e->toString;
2136 SRU::Response::Record->new(
2137 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
2138 recordData => $$explain
2144 sub return_auth_response {
2145 my ($cgi, $req, $resp) = @_;
2147 my $cql_query = decode_utf8($req->query);
2148 my $search = $req->cql->toEvergreenAuth;
2150 my $qualifier = decode_utf8($search->{qualifier});
2151 my $term = decode_utf8($search->{term});
2153 $log->info("SRU NAF search string [$cql_query] converted to "
2154 . "[$qualifier:$term]\n");
2156 my $page_size = $req->maximumRecords;
2159 # startwith deals with pages, so convert startRecord to a page number
2160 my $page = ($req->startRecord / $page_size) || 0;
2163 if ($qualifier eq "id") {
2164 $recs = [ int($term) ];
2166 my ($realaxis, $refs) = ($qualifier =~ $authority_axis_re);
2168 my $method = "open-ils.supercat.authority.browse_top.by_axis";
2169 $method .= ".refs" if $refs;
2171 $recs = $supercat->request(
2180 my $record_position = $req->startRecord;
2181 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2182 foreach my $record (@$recs) {
2183 my $marcxml = $cstore->request(
2184 'open-ils.cstore.direct.authority.record_entry.retrieve', $record
2188 SRU::Response::Record->new(
2189 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
2190 recordData => $marcxml,
2191 recordPosition => ++$record_position
2196 $resp->numberOfRecords(scalar(@$recs));
2199 =head2 get_ou($org_unit)
2201 Returns an aou object for a given actor.org_unit shortname or ID.
2206 my $org = shift || '-';
2210 $org_unit = $actor->request(
2211 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2213 } elsif ($org !~ /^\d+$/o) {
2214 $org_unit = $actor->request(
2215 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2218 $org_unit = $actor->request(
2219 'open-ils.actor.org_unit_list.search' => id => $org