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;
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);
39 $browse_types{call_number}{xml} = sub {
42 my $year = (gmtime())[5] + 1900;
45 $content .= "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>\n";
48 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
49 $cn_class =~ s/Fieldmapper-//gso;
51 my $cn_tag = "tag:open-ils.org,$year:$cn_class/".$cn->id;
52 my $cn_lib = $cn->owning_lib->shortname;
53 my $cn_label = $cn->label;
55 $cn_label =~ s/\n//gos;
56 $cn_label =~ s/'/'/go;
58 (my $ou_class = $cn->owning_lib->class_name) =~ s/::/-/gso;
59 $ou_class =~ s/Fieldmapper-//gso;
61 my $ou_tag = "tag:open-ils.org,$year:$ou_class/".$cn->owning_lib->id;
62 my $ou_name = $cn->owning_lib->name;
64 $ou_name =~ s/\n//gos;
65 $ou_name =~ s/'/'/go;
67 (my $rec_class = $cn->record->class_name) =~ s/::/-/gso;
68 $rec_class =~ s/Fieldmapper-//gso;
70 my $rec_tag = "tag:open-ils.org,$year:$rec_class/".$cn->record->id.'/'.$cn->owning_lib->shortname;
72 $content .= "<volume id='$cn_tag' lib='$cn_lib' label='$cn_label'>\n";
73 $content .= "<owning_lib xmlns='http://open-ils.org/spec/actors/v1' id='$ou_tag' name='$ou_name'/>\n";
75 my $r_doc = $parser->parse_string($cn->record->marc);
76 $r_doc->documentElement->setAttribute( id => $rec_tag );
77 $content .= $U->entityize($r_doc->documentElement->toString);
79 $content .= "</volume>\n";
82 $content .= "</volumes>\n";
83 return ("Content-type: application/xml\n\n",$content);
87 $browse_types{call_number}{html} = sub {
92 if (!$cn_browse_xslt) {
93 $cn_browse_xslt = $parser->parse_file(
94 OpenSRF::Utils::SettingsClient
96 ->config_value( dirs => 'xsl' ).
99 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
102 my (undef,$xml) = $browse_types{call_number}{xml}->($tree);
105 "Content-type: text/html\n\n",
107 $cn_browse_xslt->transform(
108 $parser->parse_string( $xml ),
123 OpenSRF::System->bootstrap_client( config_file => $bootstrap );
125 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
126 Fieldmapper->import(IDL => $idl);
128 $supercat = OpenSRF::AppSession->create('open-ils.supercat');
129 $actor = OpenSRF::AppSession->create('open-ils.actor');
130 $search = OpenSRF::AppSession->create('open-ils.search');
131 $parser = new XML::LibXML;
132 $xslt = new XML::LibXSLT;
134 $cn_browse_xslt = $parser->parse_file(
135 OpenSRF::Utils::SettingsClient
137 ->config_value( dirs => 'xsl' ).
141 $cn_browse_xslt = $xslt->parse_stylesheet( $cn_browse_xslt );
144 ->request("open-ils.supercat.record.formats")
147 $list = [ map { (keys %$_)[0] } @$list ];
148 push @$list, 'htmlholdings','html', 'marctxt', 'ris';
150 for my $browse_axis ( qw/title author subject topic series item-age/ ) {
151 for my $record_browse_format ( @$list ) {
153 my $__f = $record_browse_format;
154 my $__a = $browse_axis;
156 $browse_types{$__a}{$__f} = sub {
157 my $record_list = shift;
160 my $real_format = shift || $__f;
165 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
166 my $feed = create_record_feed( 'record', $real_format, $record_list, $unapi, $site, $real_format =~ /(-full|-uris)$/o ? 1 : 0 );
167 $feed->root( "$base/../" );
169 $feed->link( next => $next => $feed->type );
170 $feed->link( previous => $prev => $feed->type );
173 "Content-type: ". $feed->type ."; charset=utf-8\n\n",
182 =head2 parse_feed_type($type)
184 Determines whether and how a given feed type needs to be "fleshed out"
185 with holdings information.
187 The feed type could end with the string "-full", in which case we want
188 to return call numbers, copies, and URIS.
190 Or the feed type could be "-uris", in which case we want to return
191 call numbers and URIS.
193 Otherwise, we won't return any holdings.
197 sub parse_feed_type {
200 if ($type =~ /-full$/o) {
204 if ($type =~ /-uris$/o) {
208 # Otherwise, we'll return just the facts, ma'am
212 =head2 supercat_format($format_hashref, $format_type)
214 Given a reference to a hash containing the namespace_uri,
215 docs, and schema location attributes for a set of formats,
216 generate the XML description required by the supercat service.
218 We derive the base type from the format type so that we do not
219 have to populate the hash with redundant information.
223 sub supercat_format {
227 (my $base_type = $type) =~ s/(-full|-uris)$//o;
229 my $format = "<format><name>$type</name><type>application/xml</type>";
231 for my $part ( qw/namespace_uri docs schema_location/ ) {
232 $format .= "<$part>$$h{$base_type}{$part}</$part>"
233 if ($$h{$base_type}{$part});
236 $format .= '</format>';
241 =head2 unapi_format($format_hashref, $format_type)
243 Given a reference to a hash containing the namespace_uri,
244 docs, and schema location attributes for a set of formats,
245 generate the XML description required by the supercat service.
247 We derive the base type from the format type so that we do not
248 have to populate the hash with redundant information.
256 (my $base_type = $type) =~ s/(-full|-uris)$//o;
258 my $format = "<format name='$type' type='application/xml'";
260 for my $part ( qw/namespace_uri docs schema_location/ ) {
261 $format .= " $part='$$h{$base_type}{$part}'"
262 if ($$h{$base_type}{$part});
274 return Apache2::Const::DECLINED if (-e $apache->filename);
276 (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
279 ->request("open-ils.supercat.oisbn", $isbn)
282 print "Content-type: application/xml; charset=utf-8\n\n";
283 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
285 unless (exists $$list{metarecord}) {
287 return Apache2::Const::OK;
290 print "<idlist metarecord='$$list{metarecord}'>\n";
292 for ( keys %{ $$list{record_list} } ) {
293 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
294 print " <isbn record='$_'>$o</isbn>\n"
299 return Apache2::Const::OK;
305 return Apache2::Const::DECLINED if (-e $apache->filename);
310 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
311 my $rel_name = $cgi->url(-relative=>1);
312 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
315 my $url = $cgi->url(-path_info=>$add_path);
316 my $root = (split 'unapi', $url)[0];
317 my $base = (split 'unapi', $url)[0] . 'unapi';
320 my $uri = $cgi->param('id') || '';
321 my $host = $cgi->virtual_host || $cgi->server_name;
323 my $skin = $cgi->param('skin') || 'default';
324 my $locale = $cgi->param('locale') || 'en-US';
326 my $format = $cgi->param('format');
327 my $flesh_feed = parse_feed_type($format);
328 (my $base_format = $format) =~ s/(-full|-uris)$//o;
329 my ($id,$type,$command,$lib,$paging) = ('','','');
332 my $body = "Content-type: application/xml; charset=utf-8\n\n";
334 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^/]+?)(?:\[([^\]]+)\])?(?:/(.+))?}o) {
339 $type = 'metarecord' if ($1 =~ /^m/o);
342 ->request("open-ils.supercat.$type.formats")
345 if ($type eq 'record' or $type eq 'isbn') {
346 $body .= <<" FORMATS";
348 <format name='opac' type='text/html'/>
349 <format name='html' type='text/html'/>
350 <format name='htmlholdings' type='text/html'/>
351 <format name='holdings_xml' type='application/xml'/>
352 <format name='holdings_xml-full' type='application/xml'/>
353 <format name='html-full' type='text/html'/>
354 <format name='htmlholdings-full' type='text/html'/>
355 <format name='marctxt' type='text/plain'/>
356 <format name='ris' type='text/plain'/>
358 } elsif ($type eq 'metarecord') {
359 $body .= <<" FORMATS";
361 <format name='opac' type='text/html'/>
366 my ($type) = keys %$h;
367 $body .= unapi_format($h, $type);
369 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
370 $body .= unapi_format($h, "$type-full");
371 $body .= unapi_format($h, "$type-uris");
375 $body .= "</formats>\n";
379 ->request("open-ils.supercat.record.formats")
384 ->request("open-ils.supercat.metarecord.formats")
388 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
389 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
391 $body .= <<" FORMATS";
393 <format name='opac' type='text/html'/>
394 <format name='html' type='text/html'/>
395 <format name='htmlholdings' type='text/html'/>
396 <format name='holdings_xml' type='application/xml'/>
397 <format name='holdings_xml-full' type='application/xml'/>
398 <format name='html-full' type='text/html'/>
399 <format name='htmlholdings-full' type='text/html'/>
400 <format name='marctxt' type='text/plain'/>
401 <format name='ris' type='text/plain'/>
406 my ($type) = keys %$h;
407 $body .= "\t" . unapi_format($h, $type);
409 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
410 $body .= "\t" . unapi_format($h, "$type-full");
411 $body .= "\t" . unapi_format($h, "$type-uris");
415 $body .= "</formats>\n";
419 return Apache2::Const::OK;
422 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^/]+?)(?:\[([^\]]+)\])?(?:/(.+))?}o) {
427 $type = 'metarecord' if ($1 =~ /^metabib/o);
428 $type = 'isbn' if ($1 =~ /^isbn/o);
429 $type = 'call_number' if ($1 =~ /^call_number/o);
430 $type = 'acp' if ($1 =~ /^asset-copy/o);
431 $type = 'acn' if ($1 =~ /^asset-call_number/o);
432 $type = 'auri' if ($1 =~ /^asset-uri/o);
433 $command = 'retrieve';
434 $command = 'browse' if ($type eq 'call_number');
438 $paging = [split ',', $paging];
443 if (!$lib || $lib eq '-') {
444 $lib = $actor->request(
445 'open-ils.actor.org_unit_list.search' => parent_ou => undef
446 )->gather(1)->[0]->shortname;
449 my $lib_object = $actor->request(
450 'open-ils.actor.org_unit_list.search' => shortname => $lib
452 my $lib_id = $lib_object->id;
454 my $ou_types = $actor->request( 'open-ils.actor.org_types.retrieve' )->gather(1);
455 my $lib_depth = (grep { $_->id == $lib_object->ou_type } @$ou_types)[0]->depth;
457 if ($type eq 'call_number' and $command eq 'browse') {
458 print "Location: $root/browse/$base_format/call_number/$lib/$id\n\n";
462 if ($type eq 'isbn') {
463 my $rec = $supercat->request('open-ils.supercat.isbn.object.retrieve',$id)->gather(1);
465 print "Content-type: text/html; charset=utf-8\n\n";
466 $apache->custom_response( 404, <<" HTML");
469 <title>Type [$type] with id [$id] not found!</title>
473 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
484 { (keys(%$_))[0] eq $base_format }
485 @{ $supercat->request("open-ils.supercat.$type.formats")->gather(1) }
487 { $_ eq $base_format }
488 qw/opac html htmlholdings marctxt ris holdings_xml/
490 print "Content-type: text/html; charset=utf-8\n\n";
491 $apache->custom_response( 406, <<" HTML");
494 <title>Invalid format [$format] for type [$type]!</title>
498 <center>Sorry, format $format is not valid for type $type.</center>
505 if ($format eq 'opac') {
506 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id&l=$lib_id&d=$lib_depth\n\n"
507 if ($type eq 'metarecord');
508 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id&l=$lib_id&d=$lib_depth\n\n"
509 if ($type eq 'record');
511 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format) && ($type ne 'acn' && $type ne 'acp' && $type ne 'auri')) {
512 my $feed = create_record_feed(
522 print "Content-type: text/html; charset=utf-8\n\n";
523 $apache->custom_response( 404, <<" HTML");
526 <title>Type [$type] with id [$id] not found!</title>
530 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
538 $feed->creator($host);
540 $feed->link( unapi => $base) if ($flesh_feed);
542 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
543 print $U->entityize($feed->toString) . "\n";
545 return Apache2::Const::OK;
548 my $method = "open-ils.supercat.$type.$base_format.$command";
551 if ($base_format eq 'holdings_xml') {
553 if ($format !~ /-full$/o) {
560 my $req = $supercat->request($method,@params,$paging);
561 my $data = $req->gather();
563 if ($req->failed || !$data) {
564 print "Content-type: text/html; charset=utf-8\n\n";
565 $apache->custom_response( 404, <<" HTML");
568 <title>$type $id not found!</title>
572 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
579 print "Content-type: application/xml; charset=utf-8\n\n$data";
581 if ($base_format eq 'holdings_xml') {
582 while (my $c = $req->recv) {
587 return Apache2::Const::OK;
593 return Apache2::Const::DECLINED if (-e $apache->filename);
598 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
599 my $rel_name = $cgi->url(-relative=>1);
600 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
603 my $url = $cgi->url(-path_info=>$add_path);
604 my $root = (split 'supercat', $url)[0];
605 my $base = (split 'supercat', $url)[0] . 'supercat';
606 my $unapi = (split 'supercat', $url)[0] . 'unapi';
608 my $host = $cgi->virtual_host || $cgi->server_name;
610 my $path = $cgi->path_info;
611 my ($id,$type,$format,$command) = reverse split '/', $path;
612 my $flesh_feed = parse_feed_type($type);
613 (my $base_format = $format) =~ s/(-full|-uris)$//o;
615 my $skin = $cgi->param('skin') || 'default';
616 my $locale = $cgi->param('locale') || 'en-US';
618 if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
619 print "Content-type: application/xml; charset=utf-8\n";
622 ->request("open-ils.supercat.$1.formats")
630 <type>text/html</type>
633 if ($1 eq 'record' or $1 eq 'isbn') {
635 <name>htmlholdings</name>
636 <type>text/html</type>
640 <type>text/html</type>
643 <name>htmlholdings-full</name>
644 <type>text/html</type>
647 <name>html-full</name>
648 <type>text/html</type>
652 <type>text/plain</type>
656 <type>text/plain</type>
661 my ($type) = keys %$h;
662 print supercat_format($h, $type);
664 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
665 print supercat_format($h, "$type-full");
666 print supercat_format($h, "$type-uris");
671 print "</formats>\n";
673 return Apache2::Const::OK;
677 ->request("open-ils.supercat.record.formats")
682 ->request("open-ils.supercat.metarecord.formats")
686 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
687 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
692 <type>text/html</type>
695 <name>htmlholdings</name>
696 <type>text/html</type>
700 <type>text/html</type>
703 <name>htmlholdings-full</name>
704 <type>text/html</type>
707 <name>html-full</name>
708 <type>text/html</type>
712 <type>text/plain</type>
716 <type>text/plain</type>
720 my ($type) = keys %$h;
721 print supercat_format($h, $type);
723 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
724 print supercat_format($h, "$type-full");
725 print supercat_format($h, "$type-uris");
730 print "</formats>\n";
733 return Apache2::Const::OK;
736 if ($format eq 'opac') {
737 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id\n\n"
738 if ($type eq 'metarecord');
739 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id\n\n"
740 if ($type eq 'record');
743 } elsif ($base_format eq 'marc21') {
747 my $bib = $supercat->request( "open-ils.supercat.record.object.retrieve", $id )->gather(1)->[0];
749 my $r = MARC::Record->new_from_xml( $bib->marc, 'UTF-8', 'USMARC' );
750 $r->delete_field( $_ ) for ($r->field(901));
755 a => $bib->tcn_value,
756 b => $bib->tcn_source,
761 print "Content-type: application/octet-stream\n\n";
767 print "Content-type: text/html; charset=utf-8\n\n";
768 $apache->custom_response( 404, <<" HTML");
775 <center>Couldn't fetch $id as MARC21.</center>
782 return Apache2::Const::OK;
784 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
785 my $feed = create_record_feed(
793 $feed->creator($host);
797 $feed->link( unapi => $base) if ($flesh_feed);
799 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
800 print $U->entityize($feed->toString) . "\n";
802 return Apache2::Const::OK;
805 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
809 print "Content-type: text/html; charset=utf-8\n\n";
810 $apache->custom_response( 404, <<" HTML");
813 <title>$type $id not found!</title>
817 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
824 print "Content-type: application/xml; charset=utf-8\n\n";
825 print $U->entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
827 return Apache2::Const::OK;
833 return Apache2::Const::DECLINED if (-e $apache->filename);
837 my $year = (gmtime())[5] + 1900;
838 my $host = $cgi->virtual_host || $cgi->server_name;
841 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
842 my $rel_name = $cgi->url(-relative=>1);
843 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
846 my $url = $cgi->url(-path_info=>$add_path);
847 my $root = (split 'feed', $url)[0] . '/';
848 my $base = (split 'bookbag', $url)[0] . '/bookbag';
849 my $unapi = (split 'feed', $url)[0] . '/unapi';
851 my $skin = $cgi->param('skin') || 'default';
852 my $locale = $cgi->param('locale') || 'en-US';
853 my $org = $cgi->param('searchOrg');
855 my $org_unit = get_ou($org);
856 my $scope = "l=" . $org_unit->[0]->id . "&";
858 $root =~ s{(?<!http:)//}{/}go;
859 $base =~ s{(?<!http:)//}{/}go;
860 $unapi =~ s{(?<!http:)//}{/}go;
862 my $path = $cgi->path_info;
863 #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
865 my ($id,$type) = reverse split '/', $path;
866 my $flesh_feed = parse_feed_type($type);
868 my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
869 return Apache2::Const::NOT_FOUND unless($bucket);
871 my $bucket_tag = "tag:$host,$year:record_bucket/$id";
872 if ($type eq 'opac') {
873 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
874 join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
879 my $feed = create_record_feed(
882 [ map { $_->target_biblio_record_entry } @{ $bucket->items } ],
884 $org_unit->[0]->shortname,
888 $feed->id($bucket_tag);
890 $feed->title("Items in Book Bag [".$bucket->name."]");
891 $feed->creator($host);
894 $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
895 $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
896 $feed->link(html => $base . "/html-full/$id" => 'text/html');
897 $feed->link(unapi => $unapi);
901 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
902 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
907 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
908 print $U->entityize($feed->toString) . "\n";
910 return Apache2::Const::OK;
915 return Apache2::Const::DECLINED if (-e $apache->filename);
919 my $year = (gmtime())[5] + 1900;
920 my $host = $cgi->virtual_host || $cgi->server_name;
923 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
924 my $rel_name = $cgi->url(-relative=>1);
925 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
928 my $url = $cgi->url(-path_info=>$add_path);
929 my $root = (split 'feed', $url)[0];
930 my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
931 my $unapi = (split 'feed', $url)[0] . 'unapi';
933 my $skin = $cgi->param('skin') || 'default';
934 my $locale = $cgi->param('locale') || 'en-US';
935 my $org = $cgi->param('searchOrg');
937 my $org_unit = get_ou($org);
938 my $scope = "l=" . $org_unit->[0]->id . "&";
940 my $path = $cgi->path_info;
941 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
943 $path =~ s/^\/(?:feed\/)?freshmeat\///og;
945 my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
946 my $flesh_feed = parse_feed_type($type);
950 my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
952 #if ($type eq 'opac') {
953 # print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
954 # join('&', map { "rl=" . $_ } @$list) .
959 my $feed = create_record_feed( 'record', $type, $list, $unapi, $org_unit->[0]->shortname, $flesh_feed);
963 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
965 $feed->title("$limit most recent $rtype ${axis}s");
968 $feed->creator($host);
971 $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
972 $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
973 $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
974 $feed->link(unapi => $unapi);
978 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
979 join('&', map { 'rl=' . $_} @$list ),
984 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
985 print $U->entityize($feed->toString) . "\n";
987 return Apache2::Const::OK;
996 if ($version eq '1.0') {
998 Content-type: application/opensearchdescription+xml; charset=utf-8
1000 <?xml version="1.0" encoding="UTF-8"?>
1001 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
1002 <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&startPage={startPage}&startIndex={startIndex}&count={count}</Url>
1003 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
1004 <ShortName>$lib</ShortName>
1005 <LongName>Search $lib</LongName>
1006 <Description>Search the $lib OPAC by $class.</Description>
1007 <Tags>$lib book library</Tags>
1008 <SampleSearch>harry+potter</SampleSearch>
1009 <Developer>Mike Rylander for GPLS/PINES</Developer>
1010 <Contact>feedback\@open-ils.org</Contact>
1011 <SyndicationRight>open</SyndicationRight>
1012 <AdultContent>false</AdultContent>
1013 </OpenSearchDescription>
1017 Content-type: application/opensearchdescription+xml; charset=utf-8
1019 <?xml version="1.0" encoding="UTF-8"?>
1020 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
1021 <ShortName>$lib</ShortName>
1022 <Description>Search the $lib OPAC by $class.</Description>
1023 <Tags>$lib book library</Tags>
1024 <Url type="application/rss+xml"
1025 template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1026 <Url type="application/atom+xml"
1027 template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1028 <Url type="application/x-mods3+xml"
1029 template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1030 <Url type="application/x-mods+xml"
1031 template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1032 <Url type="application/x-marcxml+xml"
1033 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1034 <Url type="text/html"
1035 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1036 <LongName>Search $lib</LongName>
1037 <Query role="example" searchTerms="harry+potter" />
1038 <Developer>Mike Rylander for GPLS/PINES</Developer>
1039 <Contact>feedback\@open-ils.org</Contact>
1040 <SyndicationRight>open</SyndicationRight>
1041 <AdultContent>false</AdultContent>
1042 <Language>en-US</Language>
1043 <OutputEncoding>UTF-8</OutputEncoding>
1044 <InputEncoding>UTF-8</InputEncoding>
1045 </OpenSearchDescription>
1049 return Apache2::Const::OK;
1052 sub opensearch_feed {
1054 return Apache2::Const::DECLINED if (-e $apache->filename);
1057 my $year = (gmtime())[5] + 1900;
1059 my $host = $cgi->virtual_host || $cgi->server_name;
1062 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1063 my $rel_name = $cgi->url(-relative=>1);
1064 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1067 my $url = $cgi->url(-path_info=>$add_path);
1068 my $root = (split 'opensearch', $url)[0];
1069 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1070 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1072 my $path = $cgi->path_info;
1073 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1075 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1081 if (!$lib || $lib eq '-') {
1082 $lib = $actor->request(
1083 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1084 )->gather(1)->[0]->shortname;
1087 if ($class eq '-') {
1091 return opensearch_osd($version, $lib, $class, $base);
1095 my $page = $cgi->param('startPage') || 1;
1096 my $offset = $cgi->param('startIndex') || 1;
1097 my $limit = $cgi->param('count') || 10;
1099 $page = 1 if ($page !~ /^\d+$/);
1100 $offset = 1 if ($offset !~ /^\d+$/);
1101 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1104 $offset = ($page - 1) * $limit;
1109 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1110 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1112 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1113 $lang = '' if ($lang eq '*');
1115 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1117 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1120 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1121 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1123 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1126 $type = $cgi->param('responseType') if $cgi->param('responseType');
1129 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1133 my $kwt = $cgi->param('kw');
1134 my $tit = $cgi->param('ti');
1135 my $aut = $cgi->param('au');
1136 my $sut = $cgi->param('su');
1137 my $set = $cgi->param('se');
1139 $terms .= " " if ($terms && $kwt);
1140 $terms .= "keyword: $kwt" if ($kwt);
1141 $terms .= " " if ($terms && $tit);
1142 $terms .= "title: $tit" if ($tit);
1143 $terms .= " " if ($terms && $aut);
1144 $terms .= "author: $aut" if ($aut);
1145 $terms .= " " if ($terms && $sut);
1146 $terms .= "subject: $sut" if ($sut);
1147 $terms .= " " if ($terms && $set);
1148 $terms .= "series: $set" if ($set);
1150 if ($version eq '1.0') {
1152 } elsif ($type eq '-') {
1155 my $flesh_feed = parse_feed_type($type);
1157 $terms = decode_utf8($terms);
1158 $lang = 'eng' if ($lang eq 'en-US');
1160 $log->debug("OpenSearch terms: $terms");
1162 my $org_unit = get_ou($org);
1164 # Apostrophes break search and get indexed as spaces anyway
1165 my $safe_terms = $terms;
1166 $safe_terms =~ s{'}{ }go;
1168 my $recs = $search->request(
1169 'open-ils.search.biblio.multiclass.query' => {
1170 org_unit => $org_unit->[0]->id,
1174 sort_dir => $sortdir,
1175 default_class => $class,
1176 ($lang ? ( 'language' => $lang ) : ()),
1177 } => $safe_terms => 1
1180 $log->debug("Hits for [$terms]: $recs->{count}");
1182 my $feed = create_record_feed(
1185 [ map { $_->[0] } @{$recs->{ids}} ],
1191 $log->debug("Feed created...");
1195 $feed->search($safe_terms);
1196 $feed->class($class);
1198 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1200 $feed->creator($host);
1203 $feed->_create_node(
1204 $feed->{item_xpath},
1205 'http://a9.com/-/spec/opensearch/1.1/',
1210 $feed->_create_node(
1211 $feed->{item_xpath},
1212 'http://a9.com/-/spec/opensearch/1.1/',
1217 $feed->_create_node(
1218 $feed->{item_xpath},
1219 'http://a9.com/-/spec/opensearch/1.1/',
1224 $log->debug("...basic feed data added...");
1228 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1229 'application/opensearch+xml'
1230 ) if ($offset + $limit < $recs->{count});
1234 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1235 'application/opensearch+xml'
1240 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1241 'application/opensearch+xml'
1246 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1247 'application/rss+xml'
1252 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1253 'application/atom+xml'
1258 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1264 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1268 $feed->link( 'unapi-server' => $unapi);
1270 $log->debug("...feed links added...");
1274 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1275 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1279 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1280 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1282 $log->debug("...and feed returned.");
1284 return Apache2::Const::OK;
1287 sub create_record_feed {
1290 my $records = shift;
1293 my $lib = uc(shift()) || '-';
1295 $flesh = 1 if (!defined($flesh));
1300 my $base = $cgi->url;
1301 my $host = $cgi->virtual_host || $cgi->server_name;
1303 my $year = (gmtime())[5] + 1900;
1305 my $flesh_feed = parse_feed_type($type);
1307 $type =~ s/(-full|-uris)$//o;
1309 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1310 $feed->base($base) if ($flesh);
1311 $feed->unapi($unapi) if ($flesh);
1313 $type = 'atom' if ($type eq 'html');
1314 $type = 'marcxml' if (($type eq 'htmlholdings') || ($type eq 'marctxt') || ($type eq 'ris'));
1316 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1319 for my $record (@$records) {
1320 next unless($record);
1322 #my $rec = $record->id;
1325 my $item_tag = "tag:$host,$year:biblio-record_entry/$rec/$lib";
1326 $item_tag = "tag:$host,$year:isbn/$rec/$lib" if ($search eq 'isbn');
1328 my $xml = $supercat->request(
1329 "open-ils.supercat.$search.$type.retrieve",
1334 my $node = $feed->add_item($xml);
1338 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && $flesh) {
1339 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, ($flesh_feed eq "uris") ? 1 : 0, $paging );
1340 while ( !$r->complete ) {
1341 $xml .= join('', map {$_->content} $r->recv);
1343 $xml .= join('', map {$_->content} $r->recv);
1344 $node->add_holdings($xml);
1347 $node->id($item_tag);
1348 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1349 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh);
1350 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh);
1351 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1352 $node->link('unapi-id' => $item_tag) if ($flesh);
1360 return Apache2::Const::DECLINED if (-e $apache->filename);
1363 my $year = (gmtime())[5] + 1900;
1365 my $host = $cgi->virtual_host || $cgi->server_name;
1368 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1369 my $rel_name = $cgi->url(-relative=>1);
1370 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1373 my $url = $cgi->url(-path_info=>$add_path);
1374 my $root = (split 'browse', $url)[0];
1375 my $base = (split 'browse', $url)[0] . 'browse';
1376 my $unapi = (split 'browse', $url)[0] . 'unapi';
1378 my $path = $cgi->path_info;
1381 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1382 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1384 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1386 $site ||= $cgi->param('searchOrg');
1387 $page ||= $cgi->param('startPage') || 0;
1388 $page_size ||= $cgi->param('count') || 9;
1390 $page = 0 if ($page !~ /^-?\d+$/);
1392 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1393 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1395 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1396 warn "something's wrong...";
1397 warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1401 $string = decode_utf8($string);
1402 $string =~ s/\+/ /go;
1405 my $tree = $supercat->request(
1406 "open-ils.supercat.$axis.browse",
1413 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1415 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1416 print $header.$content;
1417 return Apache2::Const::OK;
1420 sub item_age_browse {
1422 return Apache2::Const::DECLINED if (-e $apache->filename);
1425 my $year = (gmtime())[5] + 1900;
1427 my $host = $cgi->virtual_host || $cgi->server_name;
1430 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1431 my $rel_name = $cgi->url(-relative=>1);
1432 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1435 my $url = $cgi->url(-path_info=>$add_path);
1436 my $root = (split 'browse', $url)[0];
1437 my $base = (split 'browse', $url)[0] . 'browse';
1438 my $unapi = (split 'browse', $url)[0] . 'unapi';
1440 my $path = $cgi->path_info;
1443 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1444 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1446 unless ($axis eq 'item-age') {
1447 warn "something's wrong...";
1448 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1452 $site ||= $cgi->param('searchOrg') || '-';
1453 $page ||= $cgi->param('startPage') || 1;
1454 $page_size ||= $cgi->param('count') || 10;
1456 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1458 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1459 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1461 my $recs = $supercat->request(
1462 "open-ils.supercat.new_book_list",
1468 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1470 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1471 print $header.$content;
1472 return Apache2::Const::OK;
1475 our %qualifier_map = (
1477 # Some EG qualifiers
1478 'eg.site' => 'site',
1479 'eg.sort' => 'sort',
1480 'eg.direction' => 'dir',
1481 'eg.available' => 'available',
1484 'eg.title' => 'title',
1485 'dc.title' => 'title',
1486 'bib.titleabbreviated' => 'title|abbreviated',
1487 'bib.titleuniform' => 'title|uniform',
1488 'bib.titletranslated' => 'title|translated',
1489 'bib.titlealternative' => 'title',
1490 'bib.titleseries' => 'series',
1491 'eg.series' => 'title',
1493 # Author/Name class:
1494 'eg.author' => 'author',
1495 'eg.name' => 'author',
1496 'creator' => 'author',
1497 'dc.creator' => 'author',
1498 'dc.contributer' => 'author',
1499 'dc.publisher' => 'keyword',
1500 'bib.name' => 'author',
1501 'bib.namepersonal' => 'author|personal',
1502 'bib.namepersonalfamily'=> 'author|personal',
1503 'bib.namepersonalgiven' => 'author|personal',
1504 'bib.namecorporate' => 'author|corporate',
1505 'bib.nameconference' => 'author|conference',
1508 'eg.subject' => 'subject',
1509 'dc.subject' => 'subject',
1510 'bib.subjectplace' => 'subject|geographic',
1511 'bib.subjecttitle' => 'keyword',
1512 'bib.subjectname' => 'subject|name',
1513 'bib.subjectoccupation' => 'keyword',
1516 'eg.keyword' => 'keyword',
1517 'srw.serverchoice' => 'keyword',
1520 'dc.identifier' => 'keyword',
1523 'bib.dateissued' => undef,
1524 'bib.datecreated' => undef,
1525 'bib.datevalid' => undef,
1526 'bib.datemodified' => undef,
1527 'bib.datecopyright' => undef,
1533 'dc.format' => undef,
1536 'bib.genre' => 'keyword',
1539 'bib.audience' => undef,
1542 'bib.originplace' => undef,
1545 'dc.language' => 'lang',
1548 'bib.edition' => 'keyword',
1551 'bib.volume' => 'keyword',
1552 'bib.issue' => 'keyword',
1553 'bib.startpage' => 'keyword',
1554 'bib.endpage' => 'keyword',
1557 'bib.issuance' => 'keyword',
1560 our %qualifier_ids = (
1561 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1562 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1563 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1567 our %nested_qualifier_map = (
1569 site => ['site','Evergreen Site Code (shortname)'],
1570 sort => ['sort','Sort on relevance, title, author, pubdate, create_date or edit_date'],
1571 direction => ['dir','Sort direction (asc|desc)'],
1572 available => ['available','Filter to available (true|false)'],
1574 author => ['author'],
1576 subject => ['subject'],
1577 keyword => ['keyword'],
1578 series => ['series'],
1582 creator => ['author'],
1583 contributor => ['author'],
1584 publisher => ['keyword'],
1585 subject => ['subject'],
1586 identifier => ['keyword'],
1589 language => ['lang'],
1593 titleAbbreviated => ['title'],
1594 titleUniform => ['title'],
1595 titleTranslated => ['title'],
1596 titleAlternative => ['title'],
1597 titleSeries => ['series'],
1599 # Author/Name class:
1601 namePersonal => ['author'],
1602 namePersonalFamily => ['author'],
1603 namePersonalGiven => ['author'],
1604 nameCorporate => ['author'],
1605 nameConference => ['author'],
1608 subjectPlace => ['subject'],
1609 subjectTitle => ['keyword'],
1610 subjectName => ['subject|name'],
1611 subjectOccupation => ['keyword'],
1616 dateIssued => [undef],
1617 dateCreated => [undef],
1618 dateValid => [undef],
1619 dateModified => [undef],
1620 dateCopyright => [undef],
1623 genre => ['keyword'],
1626 audience => [undef],
1629 originPlace => [undef],
1632 edition => ['keyword'],
1635 volume => ['keyword'],
1636 issue => ['keyword'],
1637 startPage => ['keyword'],
1638 endPage => ['keyword'],
1641 issuance => ['keyword'],
1644 serverChoice => ['keyword'],
1649 my $base_explain = <<XML;
1651 id="evergreen-sru-explain-full"
1652 authoritative="true"
1653 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1654 xmlns="http://explain.z3950.org/dtd/2.0/">
1655 <serverInfo transport="http" protocol="SRU" version="1.1">
1662 <title primary="true"/>
1663 <description primary="true"/>
1667 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1672 identifier="info:srw/schema/1/marcxml-v1.1"
1673 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1677 <title>MARC21Slim (marcxml)</title>
1682 <default type="numberOfRecords">50</default>
1683 <default type="contextSet">eg</default>
1684 <default type="index">keyword</default>
1685 <default type="relation">all</default>
1686 <default type="sortSchema">marcxml</default>
1687 <default type="retrieveSchema">marcxml</default>
1688 <setting type="maximumRecords">50</setting>
1689 <supports type="relationModifier">relevant</supports>
1690 <supports type="relationModifier">stem</supports>
1691 <supports type="relationModifier">fuzzy</supports>
1692 <supports type="relationModifier">word</supports>
1703 my $req = SRU::Request->newFromCGI( $cgi );
1704 my $resp = SRU::Response->newFromRequest( $req );
1706 # Find the org_unit shortname, if passed as part of the URL
1707 # http://example.com/opac/extras/sru/SHORTNAME
1708 my $url = $cgi->path_info;
1709 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1711 if ( $resp->type eq 'searchRetrieve' ) {
1713 # Older versions of Debian packages returned terms to us double-encoded,
1714 # so we had to forcefully double-decode them a second time with
1715 # an outer decode('utf8', $string) call; this seems to be resolved with
1716 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1717 my $cql_query = decode_utf8($req->query);
1718 my $search_string = decode_utf8($req->cql->toEvergreen);
1720 # Ensure the search string overrides the default site
1721 if ($shortname and $search_string !~ m#site:#) {
1722 $search_string .= " site:$shortname";
1725 my $offset = $req->startRecord;
1726 $offset-- if ($offset);
1729 my $limit = $req->maximumRecords;
1732 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1734 my $recs = $search->request(
1735 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1738 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1740 foreach my $record (@$bre) {
1741 my $marcxml = $record->marc;
1742 # Make the beast conform to a VDX-supported format
1743 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1744 # Trying to implement LIBSOL_852_A format; so much for standards
1746 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1747 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1749 # Expects the record ID in the 001
1750 $marc->delete_field($_) for ($marc->field('001'));
1751 if (!$marc->field('001')) {
1752 $marc->insert_fields_ordered(
1753 MARC::Field->new( '001', $record->id )
1756 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1757 foreach my $cn (keys %$bib_holdings) {
1758 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1759 $marc->insert_fields_ordered(
1762 a => $cp->{'location'},
1763 b => $bib_holdings->{$cn}->{'owning_lib'},
1765 d => $cp->{'circlib'},
1766 g => $cp->{'barcode'},
1767 n => $cp->{'status'},
1773 $marc->delete_field( $_ ) for ($marc->field(901));
1774 $marc->append_fields(
1777 a => $record->tcn_value,
1778 b => $record->tcn_source,
1783 $marcxml = $marc->as_xml_record();
1784 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1788 SRU::Response::Record->new(
1789 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1790 recordData => $marcxml,
1791 recordPosition => ++$offset
1796 $resp->numberOfRecords($recs->{count});
1798 } elsif ( $resp->type eq 'explain' ) {
1800 my $host = $cgi->virtual_host || $cgi->server_name;
1803 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1804 my $rel_name = $cgi->url(-relative=>1);
1805 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1807 my $base = $cgi->url(-base=>1);
1808 my $url = $cgi->url(-path_info=>$add_path);
1809 $url =~ s/^$base\///o;
1811 my $doc = $parser->parse_string($base_explain);
1812 my $e = $doc->documentElement;
1813 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
1814 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
1815 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
1817 for my $name ( keys %OpenILS::WWW::SuperCat::nested_qualifier_map ) {
1819 my $identifier = $OpenILS::WWW::SuperCat::qualifier_ids{ $name };
1821 next unless $identifier;
1823 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
1824 $set_node->setAttribute( identifier => $identifier );
1825 $set_node->setAttribute( name => $name );
1827 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
1829 for my $index ( keys %{ $OpenILS::WWW::SuperCat::nested_qualifier_map{$name} } ) {
1830 my $desc = $OpenILS::WWW::SuperCat::nested_qualifier_map{$name}{$index}[1] || $index;
1832 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
1834 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
1835 $map_node->appendChild( $name_node );
1837 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
1839 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
1840 $index_node->appendChild( $title_node );
1841 $index_node->appendChild( $map_node );
1843 $index_node->setAttribute( id => $name . '.' . $index );
1844 $title_node->appendText( $desc );
1845 $name_node->setAttribute( set => $name );
1846 $name_node->appendText($index );
1848 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
1852 $ex_doc = $e->toString;
1856 SRU::Response::Record->new(
1857 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1858 recordData => $ex_doc
1863 print $cgi->header( -type => 'application/xml' );
1864 print $U->entityize($resp->asXML) . "\n";
1865 return Apache2::Const::OK;
1870 package CQL::BooleanNode;
1874 my $left = $self->left();
1875 my $right = $self->right();
1876 my $leftStr = $left->toEvergreen;
1877 my $rightStr = $right->toEvergreen();
1879 my $op = '||' if uc $self->op() eq 'OR';
1882 return "$leftStr $rightStr";
1885 package CQL::TermNode;
1889 my $qualifier = $self->getQualifier();
1890 my $term = $self->getTerm();
1891 my $relation = $self->getRelation();
1895 my ($qset, $qname) = split(/\./, $qualifier);
1897 $log->debug("SRU toEvergreen: $qset, $qname $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0]\n");
1899 if ( exists($OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}) ) {
1900 $qualifier = $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0] || 'kw';
1903 my @modifiers = $relation->getModifiers();
1905 my $base = $relation->getBase();
1906 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
1909 foreach my $m ( @modifiers ) {
1910 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
1916 $quote_it = 0 if ( $base eq 'all' );
1917 $term = maybeQuote($term) if $quote_it;
1920 croak( "Evergreen doesn't support the $base relations" );
1928 return "$qualifier:$term";
1932 =head2 get_ou($org_unit)
1934 Returns an aou object for a given actor.org_unit shortname or ID.
1939 my $org = shift || '-';
1943 $org_unit = $actor->request(
1944 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1946 } elsif ($org !~ /^\d+$/o) {
1947 $org_unit = $actor->request(
1948 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
1951 $org_unit = $actor->request(
1952 'open-ils.actor.org_unit_list.search' => id => $org
1961 # vim: noet:ts=4:sw=4