1 package OpenILS::WWW::SuperCat;
2 use strict; use warnings;
5 use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log);
6 use APR::Const -compile => qw(:error SUCCESS);
7 use Apache2::RequestRec ();
8 use Apache2::RequestIO ();
9 use Apache2::RequestUtil;
15 use OpenSRF::EX qw(:try);
16 use OpenSRF::Utils qw/:datetime/;
17 use OpenSRF::Utils::Cache;
19 use OpenSRF::AppSession;
24 use Unicode::Normalize;
25 use OpenILS::Utils::Fieldmapper;
26 use OpenILS::WWW::SuperCat::Feed;
27 use OpenSRF::Utils::Logger qw/$logger/;
28 use OpenILS::Application::AppUtils;
31 use MARC::File::XML ( BinaryEncoding => 'UTF-8' );
33 my $log = 'OpenSRF::Utils::Logger';
34 my $U = 'OpenILS::Application::AppUtils';
36 # set the bootstrap config when this module is loaded
37 my ($bootstrap, $supercat, $actor, $parser, $search, $xslt, $cn_browse_xslt, %browse_types, %qualifier_map);
39 my $authority_axis_re = qr/^authority\.(\w+)(\.refs)?$/;
41 $browse_types{call_number}{xml} = sub {
44 my $year = (gmtime())[5] + 1900;
47 $content .= "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>\n";
50 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
51 $cn_class =~ s/Fieldmapper-//gso;
53 my $cn_tag = "tag:open-ils.org,$year:$cn_class/".$cn->id;
54 my $cn_lib = $cn->owning_lib->shortname;
55 my $cn_label = $cn->label;
56 my $cn_prefix = $cn->prefix->label;
57 my $cn_suffix = $cn->suffix->label;
59 $cn_label =~ s/\n//gos;
60 $cn_label =~ s/&/&/go;
61 $cn_label =~ s/'/'/go;
62 $cn_label =~ s/</</go;
63 $cn_label =~ s/>/>/go;
65 $cn_prefix =~ s/\n//gos;
66 $cn_prefix =~ s/&/&/go;
67 $cn_prefix =~ s/'/'/go;
68 $cn_prefix =~ s/</</go;
69 $cn_prefix =~ s/>/>/go;
71 $cn_suffix =~ s/\n//gos;
72 $cn_suffix =~ s/&/&/go;
73 $cn_suffix =~ s/'/'/go;
74 $cn_suffix =~ s/</</go;
75 $cn_suffix =~ s/>/>/go;
77 (my $ou_class = $cn->owning_lib->class_name) =~ s/::/-/gso;
78 $ou_class =~ s/Fieldmapper-//gso;
80 my $ou_tag = "tag:open-ils.org,$year:$ou_class/".$cn->owning_lib->id;
81 my $ou_name = $cn->owning_lib->name;
83 $ou_name =~ s/\n//gos;
84 $ou_name =~ s/'/'/go;
86 (my $rec_class = $cn->record->class_name) =~ s/::/-/gso;
87 $rec_class =~ s/Fieldmapper-//gso;
89 my $rec_tag = "tag:open-ils.org,$year:$rec_class/".$cn->record->id.'/'.$cn->owning_lib->shortname;
91 $content .= "<volume id='$cn_tag' lib='$cn_lib' prefix='$cn_prefix' label='$cn_label' suffix='$cn_suffix'>\n";
92 $content .= "<owning_lib xmlns='http://open-ils.org/spec/actors/v1' id='$ou_tag' name='$ou_name'/>\n";
94 my $r_doc = $parser->parse_string($cn->record->marc);
95 $r_doc->documentElement->setAttribute( id => $rec_tag );
96 $content .= $U->entityize($r_doc->documentElement->toString);
98 $content .= "</volume>\n";
101 $content .= "</volumes>\n";
102 return ("Content-type: application/xml\n\n",$content);
106 $browse_types{call_number}{html} = sub {
111 if (!$cn_browse_xslt) {
112 $cn_browse_xslt = $parser->parse_file(
113 OpenSRF::Utils::SettingsClient
115 ->config_value( dirs => 'xsl' ).
118 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
121 my (undef,$xml) = $browse_types{call_number}{xml}->($tree);
124 "Content-type: text/html\n\n",
126 $cn_browse_xslt->transform(
127 $parser->parse_string( $xml ),
142 OpenSRF::System->bootstrap_client( config_file => $bootstrap );
144 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
145 Fieldmapper->import(IDL => $idl);
147 $supercat = OpenSRF::AppSession->create('open-ils.supercat');
148 $actor = OpenSRF::AppSession->create('open-ils.actor');
149 $search = OpenSRF::AppSession->create('open-ils.search');
150 $parser = new XML::LibXML;
151 $xslt = new XML::LibXSLT;
153 $cn_browse_xslt = $parser->parse_file(
154 OpenSRF::Utils::SettingsClient
156 ->config_value( dirs => 'xsl' ).
160 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
162 %qualifier_map = %{$supercat
163 ->request("open-ils.supercat.biblio.search_aliases")
166 my %attribute_desc = (
167 site => 'Evergreen Site Code (shortname)',
168 sort => 'Sort on relevance, title, author, pubdate, create_date or edit_date',
169 dir => 'Sort direction (asc|desc)',
170 available => 'Filter to available (true|false)',
173 # Append the non-search-alias attributes to the qualifier map
190 preferred_language_weight
191 preferred_language_multiplier
193 $qualifier_map{'eg'}{$_}{'index'} = $_;
194 if (exists $attribute_desc{$_}) {
195 $qualifier_map{'eg'}{$_}{'title'} = $attribute_desc{$_};
197 $qualifier_map{'eg'}{$_}{'title'} = $_;
202 ->request("open-ils.supercat.record.formats")
205 $list = [ map { (keys %$_)[0] } @$list ];
206 push @$list, 'htmlholdings','html', 'marctxt', 'ris';
208 for my $browse_axis ( qw/title author subject topic series item-age/ ) {
209 for my $record_browse_format ( @$list ) {
211 my $__f = $record_browse_format;
212 my $__a = $browse_axis;
214 $browse_types{$__a}{$__f} = sub {
215 my $record_list = shift;
218 my $real_format = shift || $__f;
223 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
224 my $feed = create_record_feed( 'record', $real_format, $record_list, $unapi, $site, undef, $real_format =~ /(-full|-uris)$/o ? 1 : 0 );
225 $feed->root( "$base/../" );
227 $feed->link( next => $next => $feed->type );
228 $feed->link( previous => $prev => $feed->type );
231 "Content-type: ". $feed->type ."; charset=utf-8\n\n",
239 my $auth_axes = $supercat
240 ->request("open-ils.supercat.authority.browse_axis_list")
244 for my $axis ( @$auth_axes ) {
245 my $basic_axis = 'authority.' . $axis;
246 for my $browse_axis ( ($basic_axis, $basic_axis . ".refs") ) {
249 my $__a = $browse_axis;
251 $browse_types{$__a}{$__f} = sub {
252 my $record_list = shift;
255 my $real_format = shift || $__f;
260 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
261 my $feed = create_record_feed( 'authority', $real_format, $record_list, $unapi, $site, undef, $real_format =~ /-full$/o ? -1 : 0 );
262 $feed->root( "$base/../" );
263 $feed->link( next => $next => $feed->type );
264 $feed->link( previous => $prev => $feed->type );
267 "Content-type: ". $feed->type ."; charset=utf-8\n\n",
276 sub check_child_init() {
277 if (!defined $supercat || !defined $actor || !defined $search) {
278 # For some reason one (or more) of our appsessions is missing....
284 =head2 parse_feed_type($type)
286 Determines whether and how a given feed type needs to be "fleshed out"
287 with holdings information.
289 The feed type could end with the string "-full", in which case we want
290 to return call numbers, copies, and URIS.
292 Or the feed type could end with "-uris", in which case we want to return
293 call numbers and URIS.
295 Otherwise, we won't return any holdings.
299 sub parse_feed_type {
300 my $type = shift || '';
302 if ($type =~ /-full$/o) {
306 if ($type =~ /-uris$/o) {
310 # Otherwise, we'll return just the facts, ma'am
314 =head2 supercat_format($format_hashref, $format_type)
316 Given a reference to a hash containing the namespace_uri,
317 docs, and schema location attributes for a set of formats,
318 generate the XML description required by the supercat service.
320 We derive the base type from the format type so that we do not
321 have to populate the hash with redundant information.
325 sub supercat_format {
329 (my $base_type = $type) =~ s/(-full|-uris)$//o;
331 my $format = "<format><name>$type</name><type>application/xml</type>";
333 for my $part ( qw/namespace_uri docs schema_location/ ) {
334 $format .= "<$part>$$h{$base_type}{$part}</$part>"
335 if ($$h{$base_type}{$part});
338 $format .= '</format>';
343 =head2 unapi_format($format_hashref, $format_type)
345 Given a reference to a hash containing the namespace_uri,
346 docs, and schema location attributes for a set of formats,
347 generate the XML description required by the supercat service.
349 We derive the base type from the format type so that we do not
350 have to populate the hash with redundant information.
358 (my $base_type = $type) =~ s/(-full|-uris)$//o;
360 my $format = "<format name='$type' type='application/xml'";
362 for my $part ( qw/namespace_uri docs schema_location/ ) {
363 $format .= " $part='$$h{$base_type}{$part}'"
364 if ($$h{$base_type}{$part});
376 return Apache2::Const::DECLINED if (-e $apache->filename);
380 (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
383 ->request("open-ils.supercat.oisbn", $isbn)
386 print "Content-type: application/xml; charset=utf-8\n\n";
387 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
389 unless (exists $$list{metarecord}) {
391 return Apache2::Const::OK;
394 print "<idlist metarecord='$$list{metarecord}'>\n";
396 for ( keys %{ $$list{record_list} } ) {
397 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
398 print " <isbn record='$_'>$o</isbn>\n"
403 return Apache2::Const::OK;
409 return Apache2::Const::DECLINED if (-e $apache->filename);
416 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
417 my $rel_name = $cgi->url(-relative=>1);
418 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
421 my $url = $cgi->url(-path_info=>$add_path);
422 my $root = (split 'unapi', $url)[0];
423 my $base = (split 'unapi', $url)[0] . 'unapi';
426 my $uri = $cgi->param('id') || '';
427 my $host = $cgi->virtual_host || $cgi->server_name;
429 my $skin = $cgi->param('skin') || 'default';
430 my $locale = $cgi->param('locale') || 'en-US';
432 # Enable localized results of copy status, etc
433 $supercat->session_locale($locale);
435 my $format = $cgi->param('format') || '';
436 my $flesh_feed = parse_feed_type($format);
437 (my $base_format = $format) =~ s/(-full|-uris)$//o;
438 my ($id,$type,$command,$lib,$depth,$paging) = ('','record','');
439 my $body = "Content-type: application/xml; charset=utf-8\n\n";
441 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
444 ($lib,$depth) = split('/', $4);
445 $type = 'metarecord' if ($1 =~ /^m/o);
446 $type = 'authority' if ($1 =~ /^authority/o);
450 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
453 ->request("open-ils.supercat.$type.formats")
456 if ($type eq 'record' or $type eq 'isbn') {
457 $body .= <<" FORMATS";
459 <format name='opac' type='text/html'/>
460 <format name='html' type='text/html'/>
461 <format name='htmlholdings' type='text/html'/>
462 <format name='holdings_xml' type='application/xml'/>
463 <format name='holdings_xml-full' type='application/xml'/>
464 <format name='html-full' type='text/html'/>
465 <format name='htmlholdings-full' type='text/html'/>
466 <format name='marctxt' type='text/plain'/>
467 <format name='ris' type='text/plain'/>
469 } elsif ($type eq 'metarecord') {
470 $body .= <<" FORMATS";
472 <format name='opac' type='text/html'/>
475 $body .= <<" FORMATS";
481 my ($type) = keys %$h;
482 $body .= unapi_format($h, $type);
484 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
485 $body .= unapi_format($h, "$type-full");
486 $body .= unapi_format($h, "$type-uris");
490 $body .= "</formats>\n";
494 ->request("open-ils.supercat.$type.formats")
499 ->request("open-ils.supercat.metarecord.formats")
503 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
504 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
506 $body .= <<" FORMATS";
508 <format name='opac' type='text/html'/>
509 <format name='html' type='text/html'/>
510 <format name='htmlholdings' type='text/html'/>
511 <format name='holdings_xml' type='application/xml'/>
512 <format name='holdings_xml-full' type='application/xml'/>
513 <format name='html-full' type='text/html'/>
514 <format name='htmlholdings-full' type='text/html'/>
515 <format name='marctxt' type='text/plain'/>
516 <format name='ris' type='text/plain'/>
521 my ($type) = keys %$h;
522 $body .= "\t" . unapi_format($h, $type);
524 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
525 $body .= "\t" . unapi_format($h, "$type-full");
526 $body .= "\t" . unapi_format($h, "$type-uris");
530 $body .= "</formats>\n";
534 return Apache2::Const::OK;
538 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
542 ($lib,$depth) = split('/', $4);
544 $type = 'metarecord' if ($scheme =~ /^metabib/o);
545 $type = 'isbn' if ($scheme =~ /^isbn/o);
546 $type = 'acp' if ($scheme =~ /^asset-copy/o);
547 $type = 'acn' if ($scheme =~ /^asset-call_number/o);
548 $type = 'auri' if ($scheme =~ /^asset-uri/o);
549 $type = 'authority' if ($scheme =~ /^authority/o);
550 $command = 'retrieve';
551 $command = 'browse' if (grep { $scheme eq $_ } qw/call_number title author subject topic authority.title authority.author authority.subject authority.topic series item-age/);
552 $command = 'browse' if ($scheme =~ /^authority/);
556 $paging = [split ',', $paging];
561 if (!$lib || $lib eq '-') {
562 $lib = $actor->request(
563 'open-ils.actor.org_unit_list.search' => parent_ou => undef
564 )->gather(1)->[0]->shortname;
567 my ($lib_object,$lib_id,$ou_types,$lib_depth);
568 if ($type ne 'acn' && $type ne 'acp' && $type ne 'auri') {
569 $lib_object = $actor->request(
570 'open-ils.actor.org_unit_list.search' => shortname => $lib
572 $lib_id = $lib_object->id;
574 $ou_types = $actor->request( 'open-ils.actor.org_types.retrieve' )->gather(1);
575 $lib_depth = defined($depth) ? $depth : (grep { $_->id == $lib_object->ou_type } @$ou_types)[0]->depth;
578 if ($command eq 'browse') {
579 print "Location: $root/browse/$base_format/$scheme/$lib/$id\n\n";
583 if ($type eq 'isbn') {
584 my $rec = $supercat->request('open-ils.supercat.isbn.object.retrieve',$id)->gather(1);
586 # Escape user input before display
587 $command = CGI::escapeHTML($command);
588 $id = CGI::escapeHTML($id);
589 $type = CGI::escapeHTML($type);
590 $format = CGI::escapeHTML(decode_utf8($format));
592 print "Content-type: text/html; charset=utf-8\n\n";
593 $apache->custom_response( 404, <<" HTML");
596 <title>Type [$type] with id [$id] not found!</title>
600 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
611 { (keys(%$_))[0] eq $base_format }
612 @{ $supercat->request("open-ils.supercat.$type.formats")->gather(1) }
614 { $_ eq $base_format }
615 qw/opac html htmlholdings marctxt ris holdings_xml/
617 # Escape user input before display
618 $format = CGI::escapeHTML($format);
619 $type = CGI::escapeHTML($type);
621 print "Content-type: text/html; charset=utf-8\n\n";
622 $apache->custom_response( 406, <<" HTML");
625 <title>Invalid format [$format] for type [$type]!</title>
629 <center>Sorry, format $format is not valid for type $type.</center>
636 if ($format eq 'opac') {
637 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id&l=$lib_id&d=$lib_depth\n\n"
638 if ($type eq 'metarecord');
639 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id&l=$lib_id&d=$lib_depth\n\n"
640 if ($type eq 'record');
642 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format) && ($type ne 'acn' && $type ne 'acp' && $type ne 'auri')) {
643 my $feed = create_record_feed(
654 # Escape user input before display
655 $command = CGI::escapeHTML($command);
656 $id = CGI::escapeHTML($id);
657 $type = CGI::escapeHTML($type);
658 $format = CGI::escapeHTML(decode_utf8($format));
660 print "Content-type: text/html; charset=utf-8\n\n";
661 $apache->custom_response( 404, <<" HTML");
664 <title>Type [$type] with id [$id] not found!</title>
668 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
676 $feed->creator($host);
678 $feed->link( unapi => $base) if ($flesh_feed);
680 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
681 print $U->entityize($feed->toString) . "\n";
683 return Apache2::Const::OK;
686 my $method = "open-ils.supercat.$type.$base_format.$command";
688 push @params, $lib, $lib_depth, $flesh_feed, $paging if ($base_format eq 'holdings_xml');
690 # for acn, acp, etc, the "lib" pathinfo position isn't useful.
691 # however, we can have it carry extra options like no_record! (comma separated)
692 push @params, { map { ( $_ => 1 ) } split(',', $lib) } if ( grep { $type eq $_} qw/acn acp auri/);
694 my $req = $supercat->request($method,@params);
695 my $data = $req->gather();
697 if ($req->failed || !$data) {
698 # Escape user input before display
699 $command = CGI::escapeHTML($command);
700 $id = CGI::escapeHTML($id);
701 $type = CGI::escapeHTML($type);
702 $format = CGI::escapeHTML(decode_utf8($format));
704 print "Content-type: text/html; charset=utf-8\n\n";
705 $apache->custom_response( 404, <<" HTML");
708 <title>$type $id not found!</title>
712 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
719 print "Content-type: application/xml; charset=utf-8\n\n";
721 # holdings_xml format comes back to us without an XML declaration
722 # and without being entityized; fix that here
723 if ($base_format eq 'holdings_xml') {
724 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
725 print $U->entityize($data);
727 while (my $c = $req->recv) {
728 print $U->entityize($c->content);
734 return Apache2::Const::OK;
740 return Apache2::Const::DECLINED if (-e $apache->filename);
747 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
748 my $rel_name = $cgi->url(-relative=>1);
749 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
752 my $url = $cgi->url(-path_info=>$add_path);
753 my $root = (split 'supercat', $url)[0];
754 my $base = (split 'supercat', $url)[0] . 'supercat';
755 my $unapi = (split 'supercat', $url)[0] . 'unapi';
757 my $host = $cgi->virtual_host || $cgi->server_name;
759 my $path = $cgi->path_info;
760 my ($id,$type,$format,$command) = reverse split '/', $path;
761 my $flesh_feed = parse_feed_type($format);
762 (my $base_format = $format) =~ s/(-full|-uris)$//o;
764 my $skin = $cgi->param('skin') || 'default';
765 my $locale = $cgi->param('locale') || 'en-US';
767 # Enable localized results of copy status, etc
768 $supercat->session_locale($locale);
770 if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
771 print "Content-type: application/xml; charset=utf-8\n";
774 ->request("open-ils.supercat.$1.formats")
782 <type>text/html</type>
785 if ($1 eq 'record' or $1 eq 'isbn') {
787 <name>htmlholdings</name>
788 <type>text/html</type>
792 <type>text/html</type>
795 <name>htmlholdings-full</name>
796 <type>text/html</type>
799 <name>html-full</name>
800 <type>text/html</type>
804 <type>text/plain</type>
808 <type>text/plain</type>
813 my ($type) = keys %$h;
814 print supercat_format($h, $type);
816 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
817 print supercat_format($h, "$type-full");
818 print supercat_format($h, "$type-uris");
823 print "</formats>\n";
825 return Apache2::Const::OK;
829 ->request("open-ils.supercat.record.formats")
834 ->request("open-ils.supercat.metarecord.formats")
838 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
839 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
844 <type>text/html</type>
847 <name>htmlholdings</name>
848 <type>text/html</type>
852 <type>text/html</type>
855 <name>htmlholdings-full</name>
856 <type>text/html</type>
859 <name>html-full</name>
860 <type>text/html</type>
864 <type>text/plain</type>
868 <type>text/plain</type>
872 my ($type) = keys %$h;
873 print supercat_format($h, $type);
875 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
876 print supercat_format($h, "$type-full");
877 print supercat_format($h, "$type-uris");
882 print "</formats>\n";
885 return Apache2::Const::OK;
888 if ($format eq 'opac') {
889 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id\n\n"
890 if ($type eq 'metarecord');
891 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id\n\n"
892 if ($type eq 'record');
895 } elsif ($base_format eq 'marc21') {
899 my $bib = $supercat->request( "open-ils.supercat.record.object.retrieve", $id )->gather(1)->[0];
901 print "Content-type: application/octet-stream\n\n" . MARC::Record->new_from_xml( $bib->marc, 'UTF-8', 'USMARC' )->as_usmarc;
906 # Escape user input before display
907 $id = CGI::escapeHTML($id);
909 print "Content-type: text/html; charset=utf-8\n\n";
910 $apache->custom_response( 404, <<" HTML");
917 <center>Couldn't fetch $id as MARC21.</center>
924 return Apache2::Const::OK;
926 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
927 my $feed = create_record_feed(
935 $feed->creator($host);
939 $feed->link( unapi => $base) if ($flesh_feed);
941 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
942 print $U->entityize($feed->toString) . "\n";
944 return Apache2::Const::OK;
947 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
951 # Escape user input before display
952 $command = CGI::escapeHTML($command);
953 $id = CGI::escapeHTML($id);
954 $type = CGI::escapeHTML($type);
955 $format = CGI::escapeHTML(decode_utf8($format));
957 print "Content-type: text/html; charset=utf-8\n\n";
958 $apache->custom_response( 404, <<" HTML");
961 <title>$type $id not found!</title>
965 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
972 print "Content-type: application/xml; charset=utf-8\n\n";
973 print $U->entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
975 return Apache2::Const::OK;
981 return Apache2::Const::DECLINED if (-e $apache->filename);
987 my $year = (gmtime())[5] + 1900;
988 my $host = $cgi->virtual_host || $cgi->server_name;
991 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
992 my $rel_name = $cgi->url(-relative=>1);
993 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
996 my $url = $cgi->url(-path_info=>$add_path);
997 my $root = (split 'feed', $url)[0] . '/';
998 my $base = (split 'bookbag', $url)[0] . '/bookbag';
999 my $unapi = (split 'feed', $url)[0] . '/unapi';
1001 my $skin = $cgi->param('skin') || 'default';
1002 my $locale = $cgi->param('locale') || 'en-US';
1003 my $org = $cgi->param('searchOrg');
1005 # Enable localized results of copy status, etc
1006 $supercat->session_locale($locale);
1008 my $org_unit = get_ou($org);
1009 my $scope = "l=" . $org_unit->[0]->id . "&";
1011 $root =~ s{(?<!http:)//}{/}go;
1012 $base =~ s{(?<!http:)//}{/}go;
1013 $unapi =~ s{(?<!http:)//}{/}go;
1015 my $path = $cgi->path_info;
1016 #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
1018 my ($id,$type) = reverse split '/', $path;
1019 my $flesh_feed = parse_feed_type($type);
1021 my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
1022 return Apache2::Const::NOT_FOUND unless($bucket);
1024 my $bucket_tag = "tag:$host,$year:record_bucket/$id";
1025 if ($type eq 'opac') {
1026 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1027 join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
1032 # last created first
1033 my @sorted_bucket_items = sort { $b->create_time cmp $a->create_time } @{ $bucket->items };
1035 my $feed = create_record_feed(
1038 [ map { $_->target_biblio_record_entry } @sorted_bucket_items ],
1040 $org_unit->[0]->shortname,
1045 $feed->id($bucket_tag);
1047 $feed->title("Items in Book Bag [".$bucket->name."]");
1048 $feed->description($bucket->description || ("Items in Book Bag [".$bucket->name."]"));
1049 $feed->creator($host);
1052 $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
1053 $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
1054 $feed->link(html => $base . "/html-full/$id" => 'text/html');
1055 $feed->link(unapi => $unapi);
1059 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1060 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
1065 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1066 print $U->entityize($feed->toString) . "\n";
1068 return Apache2::Const::OK;
1073 return Apache2::Const::DECLINED if (-e $apache->filename);
1079 my $year = (gmtime())[5] + 1900;
1080 my $host = $cgi->virtual_host || $cgi->server_name;
1083 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1084 my $rel_name = $cgi->url(-relative=>1);
1085 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1088 my $url = $cgi->url(-path_info=>$add_path);
1089 my $root = (split 'feed', $url)[0];
1090 my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
1091 my $unapi = (split 'feed', $url)[0] . 'unapi';
1093 my $skin = $cgi->param('skin') || 'default';
1094 my $locale = $cgi->param('locale') || 'en-US';
1095 my $org = $cgi->param('searchOrg');
1097 # Enable localized results of copy status, etc
1098 $supercat->session_locale($locale);
1100 my $org_unit = get_ou($org);
1101 my $scope = "l=" . $org_unit->[0]->id . "&";
1103 my $path = $cgi->path_info;
1104 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1106 $path =~ s/^\/(?:feed\/)?freshmeat\///og;
1108 my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
1109 my $flesh_feed = parse_feed_type($type);
1112 $limit = 10 if $limit !~ /^\d+$/;
1114 my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
1116 #if ($type eq 'opac') {
1117 # print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
1118 # join('&', map { "rl=" . $_ } @$list) .
1123 my $search = 'record';
1124 if ($rtype eq 'authority') {
1125 $search = 'authority';
1127 my $feed = create_record_feed( $search, $type, $list, $unapi, $org_unit->[0]->shortname, undef, $flesh_feed);
1131 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
1133 $feed->title("$limit most recent $rtype ${axis}s");
1136 $feed->creator($host);
1139 $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
1140 $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
1141 $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
1142 $feed->link(unapi => $unapi);
1146 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1147 join('&', map { 'rl=' . $_} @$list ),
1152 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1153 print $U->entityize($feed->toString) . "\n";
1155 return Apache2::Const::OK;
1158 sub opensearch_osd {
1159 my $version = shift;
1164 if ($version eq '1.0') {
1166 Content-type: application/opensearchdescription+xml; charset=utf-8
1168 <?xml version="1.0" encoding="UTF-8"?>
1169 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
1170 <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&startPage={startPage}&startIndex={startIndex}&count={count}</Url>
1171 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
1172 <ShortName>$lib</ShortName>
1173 <LongName>Search $lib</LongName>
1174 <Description>Search the $lib OPAC by $class.</Description>
1175 <Tags>$lib book library</Tags>
1176 <SampleSearch>harry+potter</SampleSearch>
1177 <Developer>Mike Rylander for GPLS/PINES</Developer>
1178 <Contact>feedback\@open-ils.org</Contact>
1179 <SyndicationRight>open</SyndicationRight>
1180 <AdultContent>false</AdultContent>
1181 </OpenSearchDescription>
1185 Content-type: application/opensearchdescription+xml; charset=utf-8
1187 <?xml version="1.0" encoding="UTF-8"?>
1188 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
1189 <ShortName>$lib</ShortName>
1190 <Description>Search the $lib OPAC by $class.</Description>
1191 <Tags>$lib book library</Tags>
1192 <Url type="application/rss+xml"
1193 template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1194 <Url type="application/atom+xml"
1195 template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1196 <Url type="application/x-mods3+xml"
1197 template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1198 <Url type="application/x-mods+xml"
1199 template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1200 <Url type="application/x-marcxml+xml"
1201 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1202 <Url type="text/html"
1203 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1204 <LongName>Search $lib</LongName>
1205 <Query role="example" searchTerms="harry+potter" />
1206 <Developer>Mike Rylander for GPLS/PINES</Developer>
1207 <Contact>feedback\@open-ils.org</Contact>
1208 <SyndicationRight>open</SyndicationRight>
1209 <AdultContent>false</AdultContent>
1210 <Language>en-US</Language>
1211 <OutputEncoding>UTF-8</OutputEncoding>
1212 <InputEncoding>UTF-8</InputEncoding>
1213 </OpenSearchDescription>
1217 return Apache2::Const::OK;
1220 sub opensearch_feed {
1222 return Apache2::Const::DECLINED if (-e $apache->filename);
1227 my $year = (gmtime())[5] + 1900;
1229 my $host = $cgi->virtual_host || $cgi->server_name;
1232 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1233 my $rel_name = $cgi->url(-relative=>1);
1234 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1237 my $url = $cgi->url(-path_info=>$add_path);
1238 my $root = (split 'opensearch', $url)[0];
1239 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1240 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1242 my $path = $cgi->path_info;
1243 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1245 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1251 if (!$lib || $lib eq '-') {
1252 $lib = $actor->request(
1253 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1254 )->gather(1)->[0]->shortname;
1257 if ($class eq '-') {
1261 return opensearch_osd($version, $lib, $class, $base);
1265 my $page = $cgi->param('startPage') || 1;
1266 my $offset = $cgi->param('startIndex') || 1;
1267 my $limit = $cgi->param('count') || 10;
1269 $page = 1 if ($page !~ /^\d+$/);
1270 $offset = 1 if ($offset !~ /^\d+$/);
1271 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1274 $offset = ($page - 1) * $limit;
1279 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1280 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1282 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1283 $lang = '' if ($lang eq '*');
1285 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1287 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1290 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1291 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1293 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1296 $type = $cgi->param('responseType') if $cgi->param('responseType');
1299 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1303 my $kwt = $cgi->param('kw');
1304 my $tit = $cgi->param('ti');
1305 my $aut = $cgi->param('au');
1306 my $sut = $cgi->param('su');
1307 my $set = $cgi->param('se');
1309 $terms .= " " if ($terms && $kwt);
1310 $terms .= "keyword: $kwt" if ($kwt);
1311 $terms .= " " if ($terms && $tit);
1312 $terms .= "title: $tit" if ($tit);
1313 $terms .= " " if ($terms && $aut);
1314 $terms .= "author: $aut" if ($aut);
1315 $terms .= " " if ($terms && $sut);
1316 $terms .= "subject: $sut" if ($sut);
1317 $terms .= " " if ($terms && $set);
1318 $terms .= "series: $set" if ($set);
1320 if ($version eq '1.0') {
1322 } elsif ($type eq '-') {
1325 my $flesh_feed = parse_feed_type($type);
1327 $terms = decode_utf8($terms);
1328 $lang = 'eng' if ($lang eq 'en-US');
1330 $log->debug("OpenSearch terms: $terms");
1332 my $org_unit = get_ou($org);
1334 # Apostrophes break search and get indexed as spaces anyway
1335 my $safe_terms = $terms;
1336 $safe_terms =~ s{'}{ }go;
1338 my $recs = $search->request(
1339 'open-ils.search.biblio.multiclass.query' => {
1340 org_unit => $org_unit->[0]->id,
1344 sort_dir => $sortdir,
1345 default_class => $class,
1346 ($lang ? ( 'language' => $lang ) : ()),
1347 } => $safe_terms => 1
1350 $log->debug("Hits for [$terms]: $recs->{count}");
1352 my $feed = create_record_feed(
1355 [ map { $_->[0] } @{$recs->{ids}} ],
1362 $log->debug("Feed created...");
1366 $feed->search($safe_terms);
1367 $feed->class($class);
1369 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1371 $feed->creator($host);
1374 $feed->_create_node(
1375 $feed->{item_xpath},
1376 'http://a9.com/-/spec/opensearch/1.1/',
1381 $feed->_create_node(
1382 $feed->{item_xpath},
1383 'http://a9.com/-/spec/opensearch/1.1/',
1388 $feed->_create_node(
1389 $feed->{item_xpath},
1390 'http://a9.com/-/spec/opensearch/1.1/',
1395 $log->debug("...basic feed data added...");
1399 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1400 'application/opensearch+xml'
1401 ) if ($offset + $limit < $recs->{count});
1405 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1406 'application/opensearch+xml'
1411 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1412 'application/opensearch+xml'
1417 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1418 'application/rss+xml'
1423 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1424 'application/atom+xml'
1429 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1435 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1439 $feed->link( 'unapi-server' => $unapi);
1441 $log->debug("...feed links added...");
1445 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1446 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1450 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1451 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1453 $log->debug("...and feed returned.");
1455 return Apache2::Const::OK;
1458 sub create_record_feed {
1461 my $records = shift;
1464 my $lib = uc(shift()) || '-';
1471 my $base = $cgi->url;
1472 my $host = $cgi->virtual_host || $cgi->server_name;
1474 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
1478 my $tag_prefix = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d", $month, $day);
1480 my $flesh_feed = defined($flesh) ? $flesh : parse_feed_type($type);
1482 $type =~ s/(-full|-uris)$//o;
1484 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1485 $feed->base($base) if ($flesh);
1486 $feed->unapi($unapi) if ($flesh);
1488 $type = 'atom' if ($type eq 'html');
1489 $type = 'marcxml' if (($type eq 'htmlholdings') || ($type eq 'marctxt') || ($type eq 'ris'));
1491 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1494 for my $record (@$records) {
1495 next unless($record);
1497 #my $rec = $record->id;
1500 my $item_tag = "$tag_prefix:biblio-record_entry/$rec/$lib";
1501 $item_tag = "$tag_prefix:metabib-metarecord/$rec/$lib" if ($search eq 'metarecord');
1502 $item_tag = "$tag_prefix:isbn/$rec/$lib" if ($search eq 'isbn');
1503 $item_tag .= "/$depth" if (defined($depth));
1505 $item_tag = "$tag_prefix:authority-record_entry/$rec" if ($search eq 'authority');
1507 my $xml = $supercat->request(
1508 "open-ils.supercat.$search.$type.retrieve",
1513 my $node = $feed->add_item($xml);
1517 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && ($flesh > 0)) {
1518 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
1519 while ( !$r->complete ) {
1520 $xml .= join('', map {$_->content} $r->recv);
1522 $xml .= join('', map {$_->content} $r->recv);
1523 $node->add_holdings($xml);
1526 $node->id($item_tag);
1527 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1528 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
1529 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
1530 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1531 $node->link('unapi-id' => $item_tag) if ($flesh);
1539 return Apache2::Const::DECLINED if (-e $apache->filename);
1544 my $year = (gmtime())[5] + 1900;
1546 my $host = $cgi->virtual_host || $cgi->server_name;
1549 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1550 my $rel_name = $cgi->url(-relative=>1);
1551 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1554 my $url = $cgi->url(-path_info=>$add_path);
1555 my $root = (split 'browse', $url)[0];
1556 my $base = (split 'browse', $url)[0] . 'browse';
1557 my $unapi = (split 'browse', $url)[0] . 'unapi';
1559 my $path = $cgi->path_info;
1562 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1563 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1565 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1567 my $status = [$cgi->param('status')];
1568 my $cpLoc = [$cgi->param('copyLocation')];
1569 $site ||= $cgi->param('searchOrg');
1570 $page ||= $cgi->param('startPage') || 0;
1571 $page_size ||= $cgi->param('count') || 9;
1573 $page = 0 if ($page !~ /^-?\d+$/);
1574 $page_size = 9 if $page_size !~ /^\d+$/;
1576 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1577 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1579 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1580 warn "something's wrong...";
1581 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1585 $string = decode_utf8($string);
1586 $string =~ s/\+/ /go;
1590 if ($axis =~ /^authority/) {
1591 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1593 my $method = "open-ils.supercat.authority.browse_center.by_axis";
1594 $method .= ".refs" if $refs;
1596 $tree = $supercat->request(
1604 $tree = $supercat->request(
1605 "open-ils.supercat.$axis.browse",
1615 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1617 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1618 print $header.$content;
1619 return Apache2::Const::OK;
1622 sub string_startwith {
1624 return Apache2::Const::DECLINED if (-e $apache->filename);
1629 my $year = (gmtime())[5] + 1900;
1631 my $host = $cgi->virtual_host || $cgi->server_name;
1634 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1635 my $rel_name = $cgi->url(-relative=>1);
1636 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1639 my $url = $cgi->url(-path_info=>$add_path);
1640 my $root = (split 'startwith', $url)[0];
1641 my $base = (split 'startwith', $url)[0] . 'startwith';
1642 my $unapi = (split 'startwith', $url)[0] . 'unapi';
1644 my $path = $cgi->path_info;
1647 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1648 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1650 my $status = [$cgi->param('status')];
1651 my $cpLoc = [$cgi->param('copyLocation')];
1652 $site ||= $cgi->param('searchOrg');
1653 $page ||= $cgi->param('startPage') || 0;
1654 $page_size ||= $cgi->param('count') || 9;
1656 $page = 0 if ($page !~ /^-?\d+$/);
1657 $page_size = 9 if $page_size !~ /^\d+$/;
1659 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1660 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1662 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1663 warn "something's wrong...";
1664 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1668 $string = decode_utf8($string);
1669 $string =~ s/\+/ /go;
1673 if ($axis =~ /^authority/) {
1674 my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
1676 my $method = "open-ils.supercat.authority.browse_top.by_axis";
1677 $method .= ".refs" if $refs;
1679 $tree = $supercat->request(
1687 $tree = $supercat->request(
1688 "open-ils.supercat.$axis.startwith",
1698 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1700 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1701 print $header.$content;
1702 return Apache2::Const::OK;
1705 sub item_age_browse {
1707 return Apache2::Const::DECLINED if (-e $apache->filename);
1710 my $year = (gmtime())[5] + 1900;
1712 my $host = $cgi->virtual_host || $cgi->server_name;
1715 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1716 my $rel_name = $cgi->url(-relative=>1);
1717 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1720 my $url = $cgi->url(-path_info=>$add_path);
1721 my $root = (split 'browse', $url)[0];
1722 my $base = (split 'browse', $url)[0] . 'browse';
1723 my $unapi = (split 'browse', $url)[0] . 'unapi';
1725 my $path = $cgi->path_info;
1728 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1729 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1731 unless ($axis eq 'item-age') {
1732 warn "something's wrong...";
1733 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1737 my $status = [$cgi->param('status')];
1738 my $cpLoc = [$cgi->param('copyLocation')];
1739 $site ||= $cgi->param('searchOrg') || '-';
1740 $page ||= $cgi->param('startPage') || 1;
1741 $page_size ||= $cgi->param('count') || 10;
1743 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1744 $page_size = 10 if $page_size !~ /^\d+$/;
1746 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1747 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1749 my $recs = $supercat->request(
1750 "open-ils.supercat.new_book_list",
1758 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1760 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1761 print $header.$content;
1762 return Apache2::Const::OK;
1765 our %qualifier_ids = (
1766 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1767 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1768 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1772 # Our authority search options are currently pretty impoverished;
1773 # just right-truncated string match on a few categories, or by
1775 our %nested_auth_qualifier_map = (
1777 id => { index => 'id', title => 'Record number'},
1778 name => { index => 'author', title => 'Personal or corporate author, or meeting name'},
1779 title => { index => 'title', title => 'Uniform title'},
1780 subject => { index => 'subject', title => 'Chronological term, topical term, geographic name, or genre/form term'},
1781 topic => { index => 'topic', title => 'Topical term'},
1785 my $base_explain = <<XML;
1787 id="evergreen-sru-explain-full"
1788 authoritative="true"
1789 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1790 xmlns="http://explain.z3950.org/dtd/2.0/">
1791 <serverInfo transport="http" protocol="SRU" version="1.1">
1798 <title primary="true"/>
1799 <description primary="true"/>
1803 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1808 identifier="info:srw/schema/1/marcxml-v1.1"
1809 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1813 <title>MARC21Slim (marcxml)</title>
1818 <default type="numberOfRecords">10</default>
1819 <default type="contextSet">eg</default>
1820 <default type="index">keyword</default>
1821 <default type="relation">all</default>
1822 <default type="sortSchema">marcxml</default>
1823 <default type="retrieveSchema">marcxml</default>
1824 <setting type="maximumRecords">50</setting>
1825 <supports type="relationModifier">relevant</supports>
1826 <supports type="relationModifier">stem</supports>
1827 <supports type="relationModifier">fuzzy</supports>
1828 <supports type="relationModifier">word</supports>
1841 my $req = SRU::Request->newFromCGI( $cgi );
1842 my $resp = SRU::Response->newFromRequest( $req );
1844 # Find the org_unit shortname, if passed as part of the URL
1845 # http://example.com/opac/extras/sru/SHORTNAME
1846 my $url = $cgi->path_info;
1847 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1849 if ( $resp->type eq 'searchRetrieve' ) {
1851 # Older versions of Debian packages returned terms to us double-encoded,
1852 # so we had to forcefully double-decode them a second time with
1853 # an outer decode('utf8', $string) call; this seems to be resolved with
1854 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1855 my $cql_query = decode_utf8($req->query);
1856 my $search_string = decode_utf8($req->cql->toEvergreen);
1858 # Ensure the search string overrides the default site
1859 if ($shortname and $search_string !~ m#site:#) {
1860 $search_string .= " site:$shortname";
1863 my $offset = $req->startRecord;
1864 $offset-- if ($offset);
1867 my $limit = $req->maximumRecords;
1870 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1872 my $recs = $search->request(
1873 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1876 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1878 foreach my $record (@$bre) {
1879 my $marcxml = $record->marc;
1880 # Make the beast conform to a VDX-supported format
1881 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1882 # Trying to implement LIBSOL_852_A format; so much for standards
1884 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1885 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1887 # Force record leader to 'a' as our data is always UTF8
1888 # Avoids marc8_to_utf8 from being invoked with horrible results
1889 # on the off-chance the record leader isn't correct
1890 my $ldr = $marc->leader;
1891 substr($ldr, 9, 1, 'a');
1892 $marc->leader($ldr);
1894 # Expects the record ID in the 001
1895 $marc->delete_field($_) for ($marc->field('001'));
1896 if (!$marc->field('001')) {
1897 $marc->insert_fields_ordered(
1898 MARC::Field->new( '001', $record->id )
1901 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1902 foreach my $cn (keys %$bib_holdings) {
1903 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1904 $marc->insert_fields_ordered(
1907 a => $cp->{'location'},
1908 b => $bib_holdings->{$cn}->{'owning_lib'},
1910 d => $cp->{'circlib'},
1911 g => $cp->{'barcode'},
1912 n => $cp->{'status'},
1918 # Ensure the data is encoded as UTF8 before we hand it off
1919 $marcxml = encode_utf8($marc->as_xml_record());
1920 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1924 SRU::Response::Record->new(
1925 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1926 recordData => $marcxml,
1927 recordPosition => ++$offset
1932 $resp->numberOfRecords($recs->{count});
1934 } elsif ( $resp->type eq 'explain' ) {
1935 return_sru_explain($cgi, $req, $resp, \$ex_doc,
1937 \%OpenILS::WWW::SuperCat::qualifier_ids
1941 SRU::Response::Record->new(
1942 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1943 recordData => $ex_doc
1948 print $cgi->header( -type => 'application/xml' );
1949 print $U->entityize($resp->asXML) . "\n";
1950 return Apache2::Const::OK;
1955 package CQL::BooleanNode;
1959 my $left = $self->left();
1960 my $right = $self->right();
1961 my $leftStr = $left->toEvergreen;
1962 my $rightStr = $right->toEvergreen();
1964 my $op = '||' if uc $self->op() eq 'OR';
1967 return "$leftStr $rightStr";
1970 sub toEvergreenAuth {
1971 return toEvergreen(shift);
1974 package CQL::TermNode;
1978 my $qualifier = $self->getQualifier();
1979 my $term = $self->getTerm();
1980 my $relation = $self->getRelation();
1984 my ($qset, $qname) = split(/\./, $qualifier);
1986 # Per http://www.loc.gov/standards/sru/specs/cql.html
1987 # "All parts of CQL are case insensitive [...] If any case insensitive
1988 # part of CQL is specified with both upper and lower case, it is for
1989 # aesthetic purposes only."
1991 # So fold the qualifier and relation to lower case
1993 $qname = lc($qname);
1995 if ( exists($qualifier_map{$qset}{$qname}) ) {
1996 $qualifier = $qualifier_map{$qset}{$qname}{'index'} || 'kw';
1997 $log->debug("SRU toEvergreen: $qset, $qname $qualifier_map{$qset}{$qname}{'index'}\n");
2000 my @modifiers = $relation->getModifiers();
2002 my $base = $relation->getBase();
2003 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
2006 foreach my $m ( @modifiers ) {
2007 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
2013 $quote_it = 0 if ( $base eq 'all' );
2014 $term = maybeQuote($term) if $quote_it;
2017 croak( "Evergreen doesn't support the $base relations" );
2025 return "$qualifier:$term";
2028 sub toEvergreenAuth {
2030 my $qualifier = $self->getQualifier();
2031 my $term = $self->getTerm();
2032 my $relation = $self->getRelation();
2036 my ($qset, $qname) = split(/\./, $qualifier);
2038 if ( exists($OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}) ) {
2039 $qualifier = $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'} || 'author';
2040 $log->debug("SRU toEvergreenAuth: $qset, $qname $OpenILS::WWW::SuperCat::nested_auth_qualifier_map{$qset}{$qname}{'index'}\n");
2043 return { qualifier => $qualifier, term => $term };
2048 sub sru_auth_search {
2053 my $req = SRU::Request->newFromCGI( $cgi );
2054 my $resp = SRU::Response->newFromRequest( $req );
2056 if ( $resp->type eq 'searchRetrieve' ) {
2057 return_auth_response($cgi, $req, $resp);
2058 } elsif ( $resp->type eq 'explain' ) {
2059 return_sru_explain($cgi, $req, $resp, \$auth_ex_doc,
2060 \%OpenILS::WWW::SuperCat::nested_auth_qualifier_map,
2061 \%OpenILS::WWW::SuperCat::qualifier_ids
2065 print $cgi->header( -type => 'application/xml' );
2066 print $U->entityize($resp->asXML) . "\n";
2067 return Apache2::Const::OK;
2070 sub explain_header {
2073 my $host = $cgi->virtual_host || $cgi->server_name;
2076 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
2077 my $rel_name = $cgi->url(-relative=>1);
2078 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
2080 my $base = $cgi->url(-base=>1);
2081 my $url = $cgi->url(-path_info=>$add_path);
2082 $url =~ s/^$base\///o;
2084 my $doc = $parser->parse_string($base_explain);
2085 my $e = $doc->documentElement;
2086 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
2087 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
2088 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
2093 sub return_sru_explain {
2094 my ($cgi, $req, $resp, $explain, $index_map, $qualifier_ids) = @_;
2096 $index_map ||= \%qualifier_map;
2098 my ($doc, $e) = explain_header($cgi);
2099 for my $name ( keys %{$index_map} ) {
2101 my $identifier = $qualifier_ids->{ $name };
2103 next unless $identifier;
2105 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
2106 $set_node->setAttribute( identifier => $identifier );
2107 $set_node->setAttribute( name => $name );
2109 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
2110 for my $index ( sort keys %{$index_map->{$name}} ) {
2111 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
2113 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
2114 $map_node->appendChild( $name_node );
2116 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
2118 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
2119 $index_node->appendChild( $title_node );
2120 $index_node->appendChild( $map_node );
2122 $index_node->setAttribute( id => "$name.$index" );
2123 $title_node->appendText($index_map->{$name}{$index}{'title'});
2124 $name_node->setAttribute( set => $name );
2125 $name_node->appendText($index_map->{$name}{$index}{'index'});
2127 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
2131 $$explain = $e->toString;
2135 SRU::Response::Record->new(
2136 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
2137 recordData => $$explain
2143 sub return_auth_response {
2144 my ($cgi, $req, $resp) = @_;
2146 my $cql_query = decode_utf8($req->query);
2147 my $search = $req->cql->toEvergreenAuth;
2149 my $qualifier = decode_utf8($search->{qualifier});
2150 my $term = decode_utf8($search->{term});
2152 $log->info("SRU NAF search string [$cql_query] converted to "
2153 . "[$qualifier:$term]\n");
2155 my $page_size = $req->maximumRecords;
2158 # startwith deals with pages, so convert startRecord to a page number
2159 my $page = ($req->startRecord / $page_size) || 0;
2162 if ($qualifier eq "id") {
2163 $recs = [ int($term) ];
2165 my ($realaxis, $refs) = ($qualifier =~ $authority_axis_re);
2167 my $method = "open-ils.supercat.authority.browse_top.by_axis";
2168 $method .= ".refs" if $refs;
2170 $recs = $supercat->request(
2179 my $record_position = $req->startRecord;
2180 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2181 foreach my $record (@$recs) {
2182 my $marcxml = $cstore->request(
2183 'open-ils.cstore.direct.authority.record_entry.retrieve', $record
2187 SRU::Response::Record->new(
2188 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
2189 recordData => $marcxml,
2190 recordPosition => ++$record_position
2195 $resp->numberOfRecords(scalar(@$recs));
2198 =head2 get_ou($org_unit)
2200 Returns an aou object for a given actor.org_unit shortname or ID.
2205 my $org = shift || '-';
2209 $org_unit = $actor->request(
2210 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2212 } elsif ($org !~ /^\d+$/o) {
2213 $org_unit = $actor->request(
2214 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2217 $org_unit = $actor->request(
2218 'open-ils.actor.org_unit_list.search' => id => $org