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, undef, $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",
181 for my $browse_axis ( qw/authority.title authority.author authority.subject authority.topic/ ) {
182 for my $record_browse_format ( qw/marcxml/ ) {
184 my $__f = $record_browse_format;
185 my $__a = $browse_axis;
187 $browse_types{$__a}{$__f} = sub {
188 my $record_list = shift;
191 my $real_format = shift || $__f;
196 $log->info("Creating record feed with params [$real_format, $record_list, $unapi, $site]");
197 my $feed = create_record_feed( 'authority', $real_format, $record_list, $unapi, $site, undef, $real_format =~ /-full$/o ? -1 : 0 );
198 $feed->root( "$base/../" );
199 $feed->link( next => $next => $feed->type );
200 $feed->link( previous => $prev => $feed->type );
203 "Content-type: ". $feed->type ."; charset=utf-8\n\n",
212 =head2 parse_feed_type($type)
214 Determines whether and how a given feed type needs to be "fleshed out"
215 with holdings information.
217 The feed type could end with the string "-full", in which case we want
218 to return call numbers, copies, and URIS.
220 Or the feed type could be "-uris", in which case we want to return
221 call numbers and URIS.
223 Otherwise, we won't return any holdings.
227 sub parse_feed_type {
230 if ($type =~ /-full$/o) {
234 if ($type =~ /-uris$/o) {
238 # Otherwise, we'll return just the facts, ma'am
242 =head2 supercat_format($format_hashref, $format_type)
244 Given a reference to a hash containing the namespace_uri,
245 docs, and schema location attributes for a set of formats,
246 generate the XML description required by the supercat service.
248 We derive the base type from the format type so that we do not
249 have to populate the hash with redundant information.
253 sub supercat_format {
257 (my $base_type = $type) =~ s/(-full|-uris)$//o;
259 my $format = "<format><name>$type</name><type>application/xml</type>";
261 for my $part ( qw/namespace_uri docs schema_location/ ) {
262 $format .= "<$part>$$h{$base_type}{$part}</$part>"
263 if ($$h{$base_type}{$part});
266 $format .= '</format>';
271 =head2 unapi_format($format_hashref, $format_type)
273 Given a reference to a hash containing the namespace_uri,
274 docs, and schema location attributes for a set of formats,
275 generate the XML description required by the supercat service.
277 We derive the base type from the format type so that we do not
278 have to populate the hash with redundant information.
286 (my $base_type = $type) =~ s/(-full|-uris)$//o;
288 my $format = "<format name='$type' type='application/xml'";
290 for my $part ( qw/namespace_uri docs schema_location/ ) {
291 $format .= " $part='$$h{$base_type}{$part}'"
292 if ($$h{$base_type}{$part});
304 return Apache2::Const::DECLINED if (-e $apache->filename);
306 (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
309 ->request("open-ils.supercat.oisbn", $isbn)
312 print "Content-type: application/xml; charset=utf-8\n\n";
313 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
315 unless (exists $$list{metarecord}) {
317 return Apache2::Const::OK;
320 print "<idlist metarecord='$$list{metarecord}'>\n";
322 for ( keys %{ $$list{record_list} } ) {
323 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
324 print " <isbn record='$_'>$o</isbn>\n"
329 return Apache2::Const::OK;
335 return Apache2::Const::DECLINED if (-e $apache->filename);
340 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
341 my $rel_name = $cgi->url(-relative=>1);
342 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
345 my $url = $cgi->url(-path_info=>$add_path);
346 my $root = (split 'unapi', $url)[0];
347 my $base = (split 'unapi', $url)[0] . 'unapi';
350 my $uri = $cgi->param('id') || '';
351 my $host = $cgi->virtual_host || $cgi->server_name;
353 my $skin = $cgi->param('skin') || 'default';
354 my $locale = $cgi->param('locale') || 'en-US';
356 # Enable localized results of copy status, etc
357 $supercat->session_locale($locale);
359 my $format = $cgi->param('format');
360 my $flesh_feed = parse_feed_type($format);
361 (my $base_format = $format) =~ s/(-full|-uris)$//o;
362 my ($id,$type,$command,$lib,$depth,$paging) = ('','','');
365 my $body = "Content-type: application/xml; charset=utf-8\n\n";
367 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
372 $type = 'metarecord' if ($1 =~ /^m/o);
373 $type = 'authority' if ($1 =~ /^authority/o);
376 ->request("open-ils.supercat.$type.formats")
379 if ($type eq 'record' or $type eq 'isbn') {
380 $body .= <<" FORMATS";
382 <format name='opac' type='text/html'/>
383 <format name='html' type='text/html'/>
384 <format name='htmlholdings' type='text/html'/>
385 <format name='holdings_xml' type='application/xml'/>
386 <format name='holdings_xml-full' type='application/xml'/>
387 <format name='html-full' type='text/html'/>
388 <format name='htmlholdings-full' type='text/html'/>
389 <format name='marctxt' type='text/plain'/>
390 <format name='ris' type='text/plain'/>
392 } elsif ($type eq 'metarecord') {
393 $body .= <<" FORMATS";
395 <format name='opac' type='text/html'/>
400 my ($type) = keys %$h;
401 $body .= unapi_format($h, $type);
403 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
404 $body .= unapi_format($h, "$type-full");
405 $body .= unapi_format($h, "$type-uris");
409 $body .= "</formats>\n";
413 ->request("open-ils.supercat.$type.formats")
418 ->request("open-ils.supercat.metarecord.formats")
422 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
423 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
425 $body .= <<" FORMATS";
427 <format name='opac' type='text/html'/>
428 <format name='html' type='text/html'/>
429 <format name='htmlholdings' type='text/html'/>
430 <format name='holdings_xml' type='application/xml'/>
431 <format name='holdings_xml-full' type='application/xml'/>
432 <format name='html-full' type='text/html'/>
433 <format name='htmlholdings-full' type='text/html'/>
434 <format name='marctxt' type='text/plain'/>
435 <format name='ris' type='text/plain'/>
440 my ($type) = keys %$h;
441 $body .= "\t" . unapi_format($h, $type);
443 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
444 $body .= "\t" . unapi_format($h, "$type-full");
445 $body .= "\t" . unapi_format($h, "$type-uris");
449 $body .= "</formats>\n";
453 return Apache2::Const::OK;
457 if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
461 ($lib,$depth) = split('/', $4);
463 $type = 'metarecord' if ($scheme =~ /^metabib/o);
464 $type = 'isbn' if ($scheme =~ /^isbn/o);
465 $type = 'acp' if ($scheme =~ /^asset-copy/o);
466 $type = 'acn' if ($scheme =~ /^asset-call_number/o);
467 $type = 'auri' if ($scheme =~ /^asset-uri/o);
468 $type = 'authority' if ($scheme =~ /^authority/o);
469 $command = 'retrieve';
470 $command = 'browse' if (grep { $scheme eq $_ } qw/call_number title author subjet topic authority.title authority.author authority.subject authority.topic series item-age/);
474 $paging = [split ',', $paging];
479 if (!$lib || $lib eq '-') {
480 $lib = $actor->request(
481 'open-ils.actor.org_unit_list.search' => parent_ou => undef
482 )->gather(1)->[0]->shortname;
485 my $lib_object = $actor->request(
486 'open-ils.actor.org_unit_list.search' => shortname => $lib
488 my $lib_id = $lib_object->id;
490 my $ou_types = $actor->request( 'open-ils.actor.org_types.retrieve' )->gather(1);
491 my $lib_depth = $depth || (grep { $_->id == $lib_object->ou_type } @$ou_types)[0]->depth;
493 if ($command eq 'browse') {
494 print "Location: $root/browse/$base_format/$scheme/$lib/$id\n\n";
498 if ($type eq 'isbn') {
499 my $rec = $supercat->request('open-ils.supercat.isbn.object.retrieve',$id)->gather(1);
501 print "Content-type: text/html; charset=utf-8\n\n";
502 $apache->custom_response( 404, <<" HTML");
505 <title>Type [$type] with id [$id] not found!</title>
509 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
520 { (keys(%$_))[0] eq $base_format }
521 @{ $supercat->request("open-ils.supercat.$type.formats")->gather(1) }
523 { $_ eq $base_format }
524 qw/opac html htmlholdings marctxt ris holdings_xml/
526 print "Content-type: text/html; charset=utf-8\n\n";
527 $apache->custom_response( 406, <<" HTML");
530 <title>Invalid format [$format] for type [$type]!</title>
534 <center>Sorry, format $format is not valid for type $type.</center>
541 if ($format eq 'opac') {
542 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id&l=$lib_id&d=$lib_depth\n\n"
543 if ($type eq 'metarecord');
544 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id&l=$lib_id&d=$lib_depth\n\n"
545 if ($type eq 'record');
547 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format) && ($type ne 'acn' && $type ne 'acp' && $type ne 'auri')) {
548 my $feed = create_record_feed(
559 print "Content-type: text/html; charset=utf-8\n\n";
560 $apache->custom_response( 404, <<" HTML");
563 <title>Type [$type] with id [$id] not found!</title>
567 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
575 $feed->creator($host);
577 $feed->link( unapi => $base) if ($flesh_feed);
579 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
580 print $U->entityize($feed->toString) . "\n";
582 return Apache2::Const::OK;
585 my $method = "open-ils.supercat.$type.$base_format.$command";
587 push @params, $lib, $lib_depth, $flesh_feed, $paging if ($base_format eq 'holdings_xml');
589 # for acn, acp, etc, the "lib" pathinfo position isn't useful.
590 # however, we can have it carry extra options like no_record! (comma separated)
591 push @params, { map { ( $_ => 1 ) } split(',', $lib) } if ( grep { $type eq $_} qw/acn acp auri/);
593 my $req = $supercat->request($method,@params);
594 my $data = $req->gather();
596 if ($req->failed || !$data) {
597 print "Content-type: text/html; charset=utf-8\n\n";
598 $apache->custom_response( 404, <<" HTML");
601 <title>$type $id not found!</title>
605 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
612 print "Content-type: application/xml; charset=utf-8\n\n$data";
614 if ($base_format eq 'holdings_xml') {
615 while (my $c = $req->recv) {
620 return Apache2::Const::OK;
626 return Apache2::Const::DECLINED if (-e $apache->filename);
631 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
632 my $rel_name = $cgi->url(-relative=>1);
633 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
636 my $url = $cgi->url(-path_info=>$add_path);
637 my $root = (split 'supercat', $url)[0];
638 my $base = (split 'supercat', $url)[0] . 'supercat';
639 my $unapi = (split 'supercat', $url)[0] . 'unapi';
641 my $host = $cgi->virtual_host || $cgi->server_name;
643 my $path = $cgi->path_info;
644 my ($id,$type,$format,$command) = reverse split '/', $path;
645 my $flesh_feed = parse_feed_type($format);
646 (my $base_format = $format) =~ s/(-full|-uris)$//o;
648 my $skin = $cgi->param('skin') || 'default';
649 my $locale = $cgi->param('locale') || 'en-US';
651 # Enable localized results of copy status, etc
652 $supercat->session_locale($locale);
654 if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
655 print "Content-type: application/xml; charset=utf-8\n";
658 ->request("open-ils.supercat.$1.formats")
666 <type>text/html</type>
669 if ($1 eq 'record' or $1 eq 'isbn') {
671 <name>htmlholdings</name>
672 <type>text/html</type>
676 <type>text/html</type>
679 <name>htmlholdings-full</name>
680 <type>text/html</type>
683 <name>html-full</name>
684 <type>text/html</type>
688 <type>text/plain</type>
692 <type>text/plain</type>
697 my ($type) = keys %$h;
698 print supercat_format($h, $type);
700 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
701 print supercat_format($h, "$type-full");
702 print supercat_format($h, "$type-uris");
707 print "</formats>\n";
709 return Apache2::Const::OK;
713 ->request("open-ils.supercat.record.formats")
718 ->request("open-ils.supercat.metarecord.formats")
722 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
723 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
728 <type>text/html</type>
731 <name>htmlholdings</name>
732 <type>text/html</type>
736 <type>text/html</type>
739 <name>htmlholdings-full</name>
740 <type>text/html</type>
743 <name>html-full</name>
744 <type>text/html</type>
748 <type>text/plain</type>
752 <type>text/plain</type>
756 my ($type) = keys %$h;
757 print supercat_format($h, $type);
759 if (OpenILS::WWW::SuperCat::Feed->exists($type)) {
760 print supercat_format($h, "$type-full");
761 print supercat_format($h, "$type-uris");
766 print "</formats>\n";
769 return Apache2::Const::OK;
772 if ($format eq 'opac') {
773 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?m=$id\n\n"
774 if ($type eq 'metarecord');
775 print "Location: $root/../../$locale/skin/$skin/xml/rdetail.xml?r=$id\n\n"
776 if ($type eq 'record');
779 } elsif ($base_format eq 'marc21') {
783 my $bib = $supercat->request( "open-ils.supercat.record.object.retrieve", $id )->gather(1)->[0];
785 print "Content-type: application/octet-stream\n\n" . MARC::Record->new_from_xml( $bib->marc, 'UTF-8', 'USMARC' )->as_usmarc;
790 print "Content-type: text/html; charset=utf-8\n\n";
791 $apache->custom_response( 404, <<" HTML");
798 <center>Couldn't fetch $id as MARC21.</center>
805 return Apache2::Const::OK;
807 } elsif (OpenILS::WWW::SuperCat::Feed->exists($base_format)) {
808 my $feed = create_record_feed(
816 $feed->creator($host);
820 $feed->link( unapi => $base) if ($flesh_feed);
822 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
823 print $U->entityize($feed->toString) . "\n";
825 return Apache2::Const::OK;
828 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
832 print "Content-type: text/html; charset=utf-8\n\n";
833 $apache->custom_response( 404, <<" HTML");
836 <title>$type $id not found!</title>
840 <center>Sorry, we couldn't $command a $type with the id of $id in format $format.</center>
847 print "Content-type: application/xml; charset=utf-8\n\n";
848 print $U->entityize( $parser->parse_string( $req->gather(1) )->documentElement->toString );
850 return Apache2::Const::OK;
856 return Apache2::Const::DECLINED if (-e $apache->filename);
860 my $year = (gmtime())[5] + 1900;
861 my $host = $cgi->virtual_host || $cgi->server_name;
864 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
865 my $rel_name = $cgi->url(-relative=>1);
866 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
869 my $url = $cgi->url(-path_info=>$add_path);
870 my $root = (split 'feed', $url)[0] . '/';
871 my $base = (split 'bookbag', $url)[0] . '/bookbag';
872 my $unapi = (split 'feed', $url)[0] . '/unapi';
874 my $skin = $cgi->param('skin') || 'default';
875 my $locale = $cgi->param('locale') || 'en-US';
876 my $org = $cgi->param('searchOrg');
878 # Enable localized results of copy status, etc
879 $supercat->session_locale($locale);
881 my $org_unit = get_ou($org);
882 my $scope = "l=" . $org_unit->[0]->id . "&";
884 $root =~ s{(?<!http:)//}{/}go;
885 $base =~ s{(?<!http:)//}{/}go;
886 $unapi =~ s{(?<!http:)//}{/}go;
888 my $path = $cgi->path_info;
889 #warn "URL breakdown: $url -> $root -> $base -> $path -> $unapi";
891 my ($id,$type) = reverse split '/', $path;
892 my $flesh_feed = parse_feed_type($type);
894 my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
895 return Apache2::Const::NOT_FOUND unless($bucket);
897 my $bucket_tag = "tag:$host,$year:record_bucket/$id";
898 if ($type eq 'opac') {
899 print "Location: $root/../../$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
900 join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
905 my $feed = create_record_feed(
908 [ map { $_->target_biblio_record_entry } @{ $bucket->items } ],
910 $org_unit->[0]->shortname,
915 $feed->id($bucket_tag);
917 $feed->title("Items in Book Bag [".$bucket->name."]");
918 $feed->creator($host);
921 $feed->link(alternate => $base . "/rss2-full/$id" => 'application/rss+xml');
922 $feed->link(atom => $base . "/atom-full/$id" => 'application/atom+xml');
923 $feed->link(html => $base . "/html-full/$id" => 'text/html');
924 $feed->link(unapi => $unapi);
928 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
929 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
934 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
935 print $U->entityize($feed->toString) . "\n";
937 return Apache2::Const::OK;
942 return Apache2::Const::DECLINED if (-e $apache->filename);
946 my $year = (gmtime())[5] + 1900;
947 my $host = $cgi->virtual_host || $cgi->server_name;
950 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
951 my $rel_name = $cgi->url(-relative=>1);
952 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
955 my $url = $cgi->url(-path_info=>$add_path);
956 my $root = (split 'feed', $url)[0];
957 my $base = (split 'freshmeat', $url)[0] . '/freshmeat';
958 my $unapi = (split 'feed', $url)[0] . 'unapi';
960 my $skin = $cgi->param('skin') || 'default';
961 my $locale = $cgi->param('locale') || 'en-US';
962 my $org = $cgi->param('searchOrg');
964 # Enable localized results of copy status, etc
965 $supercat->session_locale($locale);
967 my $org_unit = get_ou($org);
968 my $scope = "l=" . $org_unit->[0]->id . "&";
970 my $path = $cgi->path_info;
971 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
973 $path =~ s/^\/(?:feed\/)?freshmeat\///og;
975 my ($type,$rtype,$axis,$limit,$date) = split '/', $path;
976 my $flesh_feed = parse_feed_type($type);
980 my $list = $supercat->request("open-ils.supercat.$rtype.record.$axis.recent", $date, $limit)->gather(1);
982 #if ($type eq 'opac') {
983 # print "Location: $root/../../en-US/skin/default/xml/rresult.xml?rt=list&" .
984 # join('&', map { "rl=" . $_ } @$list) .
989 my $feed = create_record_feed( 'record', $type, $list, $unapi, $org_unit->[0]->shortname, undef, $flesh_feed);
993 $feed->title("Up to $limit recent $rtype ${axis}s from $date forward");
995 $feed->title("$limit most recent $rtype ${axis}s");
998 $feed->creator($host);
1001 $feed->link(alternate => $base . "/rss2-full/$rtype/$axis/$limit/$date" => 'application/rss+xml');
1002 $feed->link(atom => $base . "/atom-full/$rtype/$axis/$limit/$date" => 'application/atom+xml');
1003 $feed->link(html => $base . "/html-full/$rtype/$axis/$limit/$date" => 'text/html');
1004 $feed->link(unapi => $unapi);
1008 "http://$host/opac/$locale/skin/$skin/xml/rresult.xml?$scope" . "rt=list&" .
1009 join('&', map { 'rl=' . $_} @$list ),
1014 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
1015 print $U->entityize($feed->toString) . "\n";
1017 return Apache2::Const::OK;
1020 sub opensearch_osd {
1021 my $version = shift;
1026 if ($version eq '1.0') {
1028 Content-type: application/opensearchdescription+xml; charset=utf-8
1030 <?xml version="1.0" encoding="UTF-8"?>
1031 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
1032 <Url>$base/1.0/$lib/-/$class/?searchTerms={searchTerms}&startPage={startPage}&startIndex={startIndex}&count={count}</Url>
1033 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
1034 <ShortName>$lib</ShortName>
1035 <LongName>Search $lib</LongName>
1036 <Description>Search the $lib OPAC by $class.</Description>
1037 <Tags>$lib book library</Tags>
1038 <SampleSearch>harry+potter</SampleSearch>
1039 <Developer>Mike Rylander for GPLS/PINES</Developer>
1040 <Contact>feedback\@open-ils.org</Contact>
1041 <SyndicationRight>open</SyndicationRight>
1042 <AdultContent>false</AdultContent>
1043 </OpenSearchDescription>
1047 Content-type: application/opensearchdescription+xml; charset=utf-8
1049 <?xml version="1.0" encoding="UTF-8"?>
1050 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
1051 <ShortName>$lib</ShortName>
1052 <Description>Search the $lib OPAC by $class.</Description>
1053 <Tags>$lib book library</Tags>
1054 <Url type="application/rss+xml"
1055 template="$base/1.1/$lib/rss2-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1056 <Url type="application/atom+xml"
1057 template="$base/1.1/$lib/atom-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1058 <Url type="application/x-mods3+xml"
1059 template="$base/1.1/$lib/mods3/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1060 <Url type="application/x-mods+xml"
1061 template="$base/1.1/$lib/mods/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1062 <Url type="application/x-marcxml+xml"
1063 template="$base/1.1/$lib/marcxml/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1064 <Url type="text/html"
1065 template="$base/1.1/$lib/html-full/$class/?searchTerms={searchTerms}&startPage={startPage?}&startIndex={startIndex?}&count={count?}&searchLang={language?}"/>
1066 <LongName>Search $lib</LongName>
1067 <Query role="example" searchTerms="harry+potter" />
1068 <Developer>Mike Rylander for GPLS/PINES</Developer>
1069 <Contact>feedback\@open-ils.org</Contact>
1070 <SyndicationRight>open</SyndicationRight>
1071 <AdultContent>false</AdultContent>
1072 <Language>en-US</Language>
1073 <OutputEncoding>UTF-8</OutputEncoding>
1074 <InputEncoding>UTF-8</InputEncoding>
1075 </OpenSearchDescription>
1079 return Apache2::Const::OK;
1082 sub opensearch_feed {
1084 return Apache2::Const::DECLINED if (-e $apache->filename);
1087 my $year = (gmtime())[5] + 1900;
1089 my $host = $cgi->virtual_host || $cgi->server_name;
1092 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1093 my $rel_name = $cgi->url(-relative=>1);
1094 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1097 my $url = $cgi->url(-path_info=>$add_path);
1098 my $root = (split 'opensearch', $url)[0];
1099 my $base = (split 'opensearch', $url)[0] . 'opensearch';
1100 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
1102 my $path = $cgi->path_info;
1103 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
1105 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
1111 if (!$lib || $lib eq '-') {
1112 $lib = $actor->request(
1113 'open-ils.actor.org_unit_list.search' => parent_ou => undef
1114 )->gather(1)->[0]->shortname;
1117 if ($class eq '-') {
1121 return opensearch_osd($version, $lib, $class, $base);
1125 my $page = $cgi->param('startPage') || 1;
1126 my $offset = $cgi->param('startIndex') || 1;
1127 my $limit = $cgi->param('count') || 10;
1129 $page = 1 if ($page !~ /^\d+$/);
1130 $offset = 1 if ($offset !~ /^\d+$/);
1131 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
1134 $offset = ($page - 1) * $limit;
1139 my ($version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = ('','','','','','','','');
1140 (undef,$version,$org,$type,$class,$terms,$sort,$sortdir,$lang) = split '/', $path;
1142 $lang = $cgi->param('searchLang') if $cgi->param('searchLang');
1143 $lang = '' if ($lang eq '*');
1145 $sort = $cgi->param('searchSort') if $cgi->param('searchSort');
1147 $sortdir = $cgi->param('searchSortDir') if $cgi->param('searchSortDir');
1150 $terms .= " " if ($terms && $cgi->param('searchTerms'));
1151 $terms .= $cgi->param('searchTerms') if $cgi->param('searchTerms');
1153 $class = $cgi->param('searchClass') if $cgi->param('searchClass');
1156 $type = $cgi->param('responseType') if $cgi->param('responseType');
1159 $org = $cgi->param('searchOrg') if $cgi->param('searchOrg');
1163 my $kwt = $cgi->param('kw');
1164 my $tit = $cgi->param('ti');
1165 my $aut = $cgi->param('au');
1166 my $sut = $cgi->param('su');
1167 my $set = $cgi->param('se');
1169 $terms .= " " if ($terms && $kwt);
1170 $terms .= "keyword: $kwt" if ($kwt);
1171 $terms .= " " if ($terms && $tit);
1172 $terms .= "title: $tit" if ($tit);
1173 $terms .= " " if ($terms && $aut);
1174 $terms .= "author: $aut" if ($aut);
1175 $terms .= " " if ($terms && $sut);
1176 $terms .= "subject: $sut" if ($sut);
1177 $terms .= " " if ($terms && $set);
1178 $terms .= "series: $set" if ($set);
1180 if ($version eq '1.0') {
1182 } elsif ($type eq '-') {
1185 my $flesh_feed = parse_feed_type($type);
1187 $terms = decode_utf8($terms);
1188 $lang = 'eng' if ($lang eq 'en-US');
1190 $log->debug("OpenSearch terms: $terms");
1192 my $org_unit = get_ou($org);
1194 # Apostrophes break search and get indexed as spaces anyway
1195 my $safe_terms = $terms;
1196 $safe_terms =~ s{'}{ }go;
1198 my $recs = $search->request(
1199 'open-ils.search.biblio.multiclass.query' => {
1200 org_unit => $org_unit->[0]->id,
1204 sort_dir => $sortdir,
1205 default_class => $class,
1206 ($lang ? ( 'language' => $lang ) : ()),
1207 } => $safe_terms => 1
1210 $log->debug("Hits for [$terms]: $recs->{count}");
1212 my $feed = create_record_feed(
1215 [ map { $_->[0] } @{$recs->{ids}} ],
1222 $log->debug("Feed created...");
1226 $feed->search($safe_terms);
1227 $feed->class($class);
1229 $feed->title("Search results for [$terms] at ".$org_unit->[0]->name);
1231 $feed->creator($host);
1234 $feed->_create_node(
1235 $feed->{item_xpath},
1236 'http://a9.com/-/spec/opensearch/1.1/',
1241 $feed->_create_node(
1242 $feed->{item_xpath},
1243 'http://a9.com/-/spec/opensearch/1.1/',
1248 $feed->_create_node(
1249 $feed->{item_xpath},
1250 'http://a9.com/-/spec/opensearch/1.1/',
1255 $log->debug("...basic feed data added...");
1259 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
1260 'application/opensearch+xml'
1261 ) if ($offset + $limit < $recs->{count});
1265 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
1266 'application/opensearch+xml'
1271 $base . "/$version/$org/$type/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1272 'application/opensearch+xml'
1277 $base . "/$version/$org/rss2-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1278 'application/rss+xml'
1283 $base . "/$version/$org/atom-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1284 'application/atom+xml'
1289 $base . "/$version/$org/html/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1295 $base . "/$version/$org/html-full/$class?searchTerms=$terms&searchSort=$sort&searchSortDir=$sortdir&searchLang=$lang" =>
1299 $feed->link( 'unapi-server' => $unapi);
1301 $log->debug("...feed links added...");
1305 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
1306 # join('&', map { 'rl=' . $_->[0] } grep { ref $_ && defined $_->[0] } @{$recs->{ids}} ),
1310 #print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . entityize($feed->toString) . "\n";
1311 print $cgi->header( -type => $feed->type, -charset => 'UTF-8') . $feed->toString . "\n";
1313 $log->debug("...and feed returned.");
1315 return Apache2::Const::OK;
1318 sub create_record_feed {
1321 my $records = shift;
1324 my $lib = uc(shift()) || '-';
1327 $flesh = 1 if (!defined($flesh));
1332 my $base = $cgi->url;
1333 my $host = $cgi->virtual_host || $cgi->server_name;
1335 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
1339 my $tag_prefix = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d", $month, $day);
1341 my $flesh_feed = parse_feed_type($type);
1343 $type =~ s/(-full|-uris)$//o;
1345 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
1346 $feed->base($base) if ($flesh);
1347 $feed->unapi($unapi) if ($flesh);
1349 $type = 'atom' if ($type eq 'html');
1350 $type = 'marcxml' if (($type eq 'htmlholdings') || ($type eq 'marctxt') || ($type eq 'ris'));
1352 #$records = $supercat->request( "open-ils.supercat.record.object.retrieve", $records )->gather(1);
1355 for my $record (@$records) {
1356 next unless($record);
1358 #my $rec = $record->id;
1361 my $item_tag = "$tag_prefix:biblio-record_entry/$rec/$lib";
1362 $item_tag = "$tag_prefix:metabib-metarecord/$rec/$lib" if ($search eq 'metarecord');
1363 $item_tag = "$tag_prefix:isbn/$rec/$lib" if ($search eq 'isbn');
1364 $item_tag .= "/$depth" if (defined($depth));
1366 $item_tag = "$tag_prefix:authority-record_entry/$rec" if ($search eq 'authority');
1368 my $xml = $supercat->request(
1369 "open-ils.supercat.$search.$type.retrieve",
1374 my $node = $feed->add_item($xml);
1378 if ($lib && ($type eq 'marcxml' || $type eq 'atom') && $flesh > 0) {
1379 my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
1380 while ( !$r->complete ) {
1381 $xml .= join('', map {$_->content} $r->recv);
1383 $xml .= join('', map {$_->content} $r->recv);
1384 $node->add_holdings($xml);
1387 $node->id($item_tag);
1388 #$node->update_ts(cleanse_ISO8601($record->edit_date));
1389 $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
1390 $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
1391 $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
1392 $node->link('unapi-id' => $item_tag) if ($flesh);
1400 return Apache2::Const::DECLINED if (-e $apache->filename);
1403 my $year = (gmtime())[5] + 1900;
1405 my $host = $cgi->virtual_host || $cgi->server_name;
1408 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1409 my $rel_name = $cgi->url(-relative=>1);
1410 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1413 my $url = $cgi->url(-path_info=>$add_path);
1414 my $root = (split 'browse', $url)[0];
1415 my $base = (split 'browse', $url)[0] . 'browse';
1416 my $unapi = (split 'browse', $url)[0] . 'unapi';
1418 my $path = $cgi->path_info;
1421 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1422 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1424 return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
1426 my $status = [$cgi->param('status')];
1427 my $cpLoc = [$cgi->param('copyLocation')];
1428 $site ||= $cgi->param('searchOrg');
1429 $page ||= $cgi->param('startPage') || 0;
1430 $page_size ||= $cgi->param('count') || 9;
1432 $page = 0 if ($page !~ /^-?\d+$/);
1434 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1435 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1437 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1438 warn "something's wrong...";
1439 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1443 $string = decode_utf8($string);
1444 $string =~ s/\+/ /go;
1447 my $tree = $supercat->request(
1448 "open-ils.supercat.$axis.browse",
1450 (($axis =~ /^authority/) ? () : ($site)),
1457 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1459 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1460 print $header.$content;
1461 return Apache2::Const::OK;
1464 sub string_startwith {
1466 return Apache2::Const::DECLINED if (-e $apache->filename);
1469 my $year = (gmtime())[5] + 1900;
1471 my $host = $cgi->virtual_host || $cgi->server_name;
1474 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1475 my $rel_name = $cgi->url(-relative=>1);
1476 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1479 my $url = $cgi->url(-path_info=>$add_path);
1480 my $root = (split 'startwith', $url)[0];
1481 my $base = (split 'startwith', $url)[0] . 'startwith';
1482 my $unapi = (split 'startwith', $url)[0] . 'unapi';
1484 my $path = $cgi->path_info;
1487 my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
1488 #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
1490 my $status = [$cgi->param('status')];
1491 my $cpLoc = [$cgi->param('copyLocation')];
1492 $site ||= $cgi->param('searchOrg');
1493 $page ||= $cgi->param('startPage') || 0;
1494 $page_size ||= $cgi->param('count') || 9;
1496 $page = 0 if ($page !~ /^-?\d+$/);
1498 my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
1499 my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
1501 unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
1502 warn "something's wrong...";
1503 warn " >>> format: $format -> axis: $axis -> site: $site -> string: $string -> page: $page -> page_size: $page_size ";
1507 $string = decode_utf8($string);
1508 $string =~ s/\+/ /go;
1511 my $tree = $supercat->request(
1512 "open-ils.supercat.$axis.startwith",
1514 (($axis =~ /^authority/) ? () : ($site)),
1521 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1523 my ($header,$content) = $browse_types{$axis}{$norm_format}->($tree,$prev,$next,$format,$unapi,$base,$site);
1524 print $header.$content;
1525 return Apache2::Const::OK;
1528 sub item_age_browse {
1530 return Apache2::Const::DECLINED if (-e $apache->filename);
1533 my $year = (gmtime())[5] + 1900;
1535 my $host = $cgi->virtual_host || $cgi->server_name;
1538 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1539 my $rel_name = $cgi->url(-relative=>1);
1540 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1543 my $url = $cgi->url(-path_info=>$add_path);
1544 my $root = (split 'browse', $url)[0];
1545 my $base = (split 'browse', $url)[0] . 'browse';
1546 my $unapi = (split 'browse', $url)[0] . 'unapi';
1548 my $path = $cgi->path_info;
1551 my ($format,$axis,$site,$page,$page_size) = split '/', $path;
1552 #warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1554 unless ($axis eq 'item-age') {
1555 warn "something's wrong...";
1556 warn " >>> $format -> $axis -> $site -> $page -> $page_size ";
1560 my $status = [$cgi->param('status')];
1561 my $cpLoc = [$cgi->param('copyLocation')];
1562 $site ||= $cgi->param('searchOrg') || '-';
1563 $page ||= $cgi->param('startPage') || 1;
1564 $page_size ||= $cgi->param('count') || 10;
1566 $page = 1 if ($page !~ /^-?\d+$/ || $page < 1);
1568 my $prev = join('/', $base,$format,$axis,$site,$page - 1,$page_size);
1569 my $next = join('/', $base,$format,$axis,$site,$page + 1,$page_size);
1571 my $recs = $supercat->request(
1572 "open-ils.supercat.new_book_list",
1580 (my $norm_format = $format) =~ s/(-full|-uris)$//o;
1582 my ($header,$content) = $browse_types{$axis}{$norm_format}->($recs,$prev,$next,$format,$unapi,$base,$site);
1583 print $header.$content;
1584 return Apache2::Const::OK;
1587 our %qualifier_map = (
1589 # Some EG qualifiers
1590 'eg.site' => 'site',
1591 'eg.sort' => 'sort',
1592 'eg.direction' => 'dir',
1593 'eg.available' => 'available',
1596 'eg.title' => 'title',
1597 'dc.title' => 'title',
1598 'bib.titleabbreviated' => 'title|abbreviated',
1599 'bib.titleuniform' => 'title|uniform',
1600 'bib.titletranslated' => 'title|translated',
1601 'bib.titlealternative' => 'title',
1602 'bib.titleseries' => 'series',
1603 'eg.series' => 'title',
1605 # Author/Name class:
1606 'eg.author' => 'author',
1607 'eg.name' => 'author',
1608 'creator' => 'author',
1609 'dc.creator' => 'author',
1610 'dc.contributer' => 'author',
1611 'dc.publisher' => 'keyword',
1612 'bib.name' => 'author',
1613 'bib.namepersonal' => 'author|personal',
1614 'bib.namepersonalfamily'=> 'author|personal',
1615 'bib.namepersonalgiven' => 'author|personal',
1616 'bib.namecorporate' => 'author|corporate',
1617 'bib.nameconference' => 'author|conference',
1620 'eg.subject' => 'subject',
1621 'dc.subject' => 'subject',
1622 'bib.subjectplace' => 'subject|geographic',
1623 'bib.subjecttitle' => 'keyword',
1624 'bib.subjectname' => 'subject|name',
1625 'bib.subjectoccupation' => 'keyword',
1628 'eg.keyword' => 'keyword',
1629 'srw.serverchoice' => 'keyword',
1632 'dc.identifier' => 'keyword',
1635 'bib.dateissued' => undef,
1636 'bib.datecreated' => undef,
1637 'bib.datevalid' => undef,
1638 'bib.datemodified' => undef,
1639 'bib.datecopyright' => undef,
1645 'dc.format' => undef,
1648 'bib.genre' => 'keyword',
1651 'bib.audience' => undef,
1654 'bib.originplace' => undef,
1657 'dc.language' => 'lang',
1660 'bib.edition' => 'keyword',
1663 'bib.volume' => 'keyword',
1664 'bib.issue' => 'keyword',
1665 'bib.startpage' => 'keyword',
1666 'bib.endpage' => 'keyword',
1669 'bib.issuance' => 'keyword',
1672 our %qualifier_ids = (
1673 eg => 'http://open-ils.org/spec/SRU/context-set/evergreen/v1',
1674 dc => 'info:srw/cql-context-set/1/dc-v1.1',
1675 bib => 'info:srw/cql-context-set/1/bib-v1.0',
1679 our %nested_qualifier_map = (
1681 site => ['site','Evergreen Site Code (shortname)'],
1682 sort => ['sort','Sort on relevance, title, author, pubdate, create_date or edit_date'],
1683 direction => ['dir','Sort direction (asc|desc)'],
1684 available => ['available','Filter to available (true|false)'],
1686 author => ['author'],
1688 subject => ['subject'],
1689 keyword => ['keyword'],
1690 series => ['series'],
1694 creator => ['author'],
1695 contributor => ['author'],
1696 publisher => ['keyword'],
1697 subject => ['subject'],
1698 identifier => ['keyword'],
1701 language => ['lang'],
1705 titleAbbreviated => ['title'],
1706 titleUniform => ['title'],
1707 titleTranslated => ['title'],
1708 titleAlternative => ['title'],
1709 titleSeries => ['series'],
1711 # Author/Name class:
1713 namePersonal => ['author'],
1714 namePersonalFamily => ['author'],
1715 namePersonalGiven => ['author'],
1716 nameCorporate => ['author'],
1717 nameConference => ['author'],
1720 subjectPlace => ['subject'],
1721 subjectTitle => ['keyword'],
1722 subjectName => ['subject|name'],
1723 subjectOccupation => ['keyword'],
1728 dateIssued => [undef],
1729 dateCreated => [undef],
1730 dateValid => [undef],
1731 dateModified => [undef],
1732 dateCopyright => [undef],
1735 genre => ['keyword'],
1738 audience => [undef],
1741 originPlace => [undef],
1744 edition => ['keyword'],
1747 volume => ['keyword'],
1748 issue => ['keyword'],
1749 startPage => ['keyword'],
1750 endPage => ['keyword'],
1753 issuance => ['keyword'],
1756 serverChoice => ['keyword'],
1761 my $base_explain = <<XML;
1763 id="evergreen-sru-explain-full"
1764 authoritative="true"
1765 xmlns:z="http://explain.z3950.org/dtd/2.0/"
1766 xmlns="http://explain.z3950.org/dtd/2.0/">
1767 <serverInfo transport="http" protocol="SRU" version="1.1">
1774 <title primary="true"/>
1775 <description primary="true"/>
1779 <set identifier="info:srw/cql-context-set/1/cql-v1.2" name="cql"/>
1784 identifier="info:srw/schema/1/marcxml-v1.1"
1785 location="http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1789 <title>MARC21Slim (marcxml)</title>
1794 <default type="numberOfRecords">50</default>
1795 <default type="contextSet">eg</default>
1796 <default type="index">keyword</default>
1797 <default type="relation">all</default>
1798 <default type="sortSchema">marcxml</default>
1799 <default type="retrieveSchema">marcxml</default>
1800 <setting type="maximumRecords">50</setting>
1801 <supports type="relationModifier">relevant</supports>
1802 <supports type="relationModifier">stem</supports>
1803 <supports type="relationModifier">fuzzy</supports>
1804 <supports type="relationModifier">word</supports>
1815 my $req = SRU::Request->newFromCGI( $cgi );
1816 my $resp = SRU::Response->newFromRequest( $req );
1818 # Find the org_unit shortname, if passed as part of the URL
1819 # http://example.com/opac/extras/sru/SHORTNAME
1820 my $url = $cgi->path_info;
1821 my ($shortname, $holdings) = $url =~ m#/?([^/]*)(/holdings)?#;
1823 if ( $resp->type eq 'searchRetrieve' ) {
1825 # Older versions of Debian packages returned terms to us double-encoded,
1826 # so we had to forcefully double-decode them a second time with
1827 # an outer decode('utf8', $string) call; this seems to be resolved with
1828 # Debian Lenny packages sometime between 2009-07-27 and 2010-02-15
1829 my $cql_query = decode_utf8($req->query);
1830 my $search_string = decode_utf8($req->cql->toEvergreen);
1832 # Ensure the search string overrides the default site
1833 if ($shortname and $search_string !~ m#site:#) {
1834 $search_string .= " site:$shortname";
1837 my $offset = $req->startRecord;
1838 $offset-- if ($offset);
1841 my $limit = $req->maximumRecords;
1844 $log->info("SRU search string [$cql_query] converted to [$search_string]\n");
1846 my $recs = $search->request(
1847 'open-ils.search.biblio.multiclass.query' => {offset => $offset, limit => $limit} => $search_string => 1
1850 my $bre = $supercat->request( 'open-ils.supercat.record.object.retrieve' => [ map { $_->[0] } @{$recs->{ids}} ] )->gather(1);
1852 foreach my $record (@$bre) {
1853 my $marcxml = $record->marc;
1854 # Make the beast conform to a VDX-supported format
1855 # See http://vdxipedia.oclc.org/index.php/Holdings_Parsing
1856 # Trying to implement LIBSOL_852_A format; so much for standards
1858 my $bib_holdings = $supercat->request('open-ils.supercat.record.basic_holdings.retrieve', $record->id, $shortname || '-')->gather(1);
1859 my $marc = MARC::Record->new_from_xml($marcxml, 'UTF8', 'XML');
1861 # Force record leader to 'a' as our data is always UTF8
1862 # Avoids marc8_to_utf8 from being invoked with horrible results
1863 # on the off-chance the record leader isn't correct
1864 my $ldr = $marc->leader;
1865 substr($ldr, 9, 1, 'a');
1866 $marc->leader($ldr);
1868 # Expects the record ID in the 001
1869 $marc->delete_field($_) for ($marc->field('001'));
1870 if (!$marc->field('001')) {
1871 $marc->insert_fields_ordered(
1872 MARC::Field->new( '001', $record->id )
1875 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
1876 foreach my $cn (keys %$bib_holdings) {
1877 foreach my $cp (@{$bib_holdings->{$cn}->{'copies'}}) {
1878 $marc->insert_fields_ordered(
1881 a => $cp->{'location'},
1882 b => $bib_holdings->{$cn}->{'owning_lib'},
1884 d => $cp->{'circlib'},
1885 g => $cp->{'barcode'},
1886 n => $cp->{'status'},
1892 # Ensure the data is encoded as UTF8 before we hand it off
1893 $marcxml = encode_utf8($marc->as_xml_record());
1894 $marcxml =~ s/^<\?xml version="1.0" encoding="UTF-8"\?>//o;
1898 SRU::Response::Record->new(
1899 recordSchema => 'info:srw/schema/1/marcxml-v1.1',
1900 recordData => $marcxml,
1901 recordPosition => ++$offset
1906 $resp->numberOfRecords($recs->{count});
1908 } elsif ( $resp->type eq 'explain' ) {
1910 my $host = $cgi->virtual_host || $cgi->server_name;
1913 if ( $cgi->server_software !~ m|^Apache/2.2| ) {
1914 my $rel_name = $cgi->url(-relative=>1);
1915 $add_path = 1 if ($cgi->url(-path_info=>1) !~ /$rel_name$/);
1917 my $base = $cgi->url(-base=>1);
1918 my $url = $cgi->url(-path_info=>$add_path);
1919 $url =~ s/^$base\///o;
1921 my $doc = $parser->parse_string($base_explain);
1922 my $e = $doc->documentElement;
1923 $e->findnodes('/z:explain/z:serverInfo/z:host')->shift->appendText( $host );
1924 $e->findnodes('/z:explain/z:serverInfo/z:port')->shift->appendText( $cgi->server_port );
1925 $e->findnodes('/z:explain/z:serverInfo/z:database')->shift->appendText( $url );
1927 for my $name ( keys %OpenILS::WWW::SuperCat::nested_qualifier_map ) {
1929 my $identifier = $OpenILS::WWW::SuperCat::qualifier_ids{ $name };
1931 next unless $identifier;
1933 my $set_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'set' );
1934 $set_node->setAttribute( identifier => $identifier );
1935 $set_node->setAttribute( name => $name );
1937 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $set_node );
1939 for my $index ( keys %{ $OpenILS::WWW::SuperCat::nested_qualifier_map{$name} } ) {
1940 my $desc = $OpenILS::WWW::SuperCat::nested_qualifier_map{$name}{$index}[1] || $index;
1942 my $name_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'name' );
1944 my $map_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'map' );
1945 $map_node->appendChild( $name_node );
1947 my $title_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'title' );
1949 my $index_node = $doc->createElementNS( 'http://explain.z3950.org/dtd/2.0/', 'index' );
1950 $index_node->appendChild( $title_node );
1951 $index_node->appendChild( $map_node );
1953 $index_node->setAttribute( id => $name . '.' . $index );
1954 $title_node->appendText( $desc );
1955 $name_node->setAttribute( set => $name );
1956 $name_node->appendText($index );
1958 $e->findnodes('/z:explain/z:indexInfo')->shift->appendChild( $index_node );
1962 $ex_doc = $e->toString;
1966 SRU::Response::Record->new(
1967 recordSchema => 'info:srw/cql-context-set/2/zeerex-1.1',
1968 recordData => $ex_doc
1973 print $cgi->header( -type => 'application/xml' );
1974 print $U->entityize($resp->asXML) . "\n";
1975 return Apache2::Const::OK;
1980 package CQL::BooleanNode;
1984 my $left = $self->left();
1985 my $right = $self->right();
1986 my $leftStr = $left->toEvergreen;
1987 my $rightStr = $right->toEvergreen();
1989 my $op = '||' if uc $self->op() eq 'OR';
1992 return "$leftStr $rightStr";
1995 package CQL::TermNode;
1999 my $qualifier = $self->getQualifier();
2000 my $term = $self->getTerm();
2001 my $relation = $self->getRelation();
2005 my ($qset, $qname) = split(/\./, $qualifier);
2007 $log->debug("SRU toEvergreen: $qset, $qname $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0]\n");
2009 if ( exists($OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}) ) {
2010 $qualifier = $OpenILS::WWW::SuperCat::nested_qualifier_map{$qset}{$qname}[0] || 'kw';
2013 my @modifiers = $relation->getModifiers();
2015 my $base = $relation->getBase();
2016 if ( grep { $base eq $_ } qw/= scr exact all/ ) {
2019 foreach my $m ( @modifiers ) {
2020 if( grep { $m->[ 1 ] eq $_ } qw/cql.fuzzy cql.stem cql.relevant cql.word/ ) {
2026 $quote_it = 0 if ( $base eq 'all' );
2027 $term = maybeQuote($term) if $quote_it;
2030 croak( "Evergreen doesn't support the $base relations" );
2038 return "$qualifier:$term";
2042 =head2 get_ou($org_unit)
2044 Returns an aou object for a given actor.org_unit shortname or ID.
2049 my $org = shift || '-';
2053 $org_unit = $actor->request(
2054 'open-ils.actor.org_unit_list.search' => parent_ou => undef
2056 } elsif ($org !~ /^\d+$/o) {
2057 $org_unit = $actor->request(
2058 'open-ils.actor.org_unit_list.search' => shortname => uc($org)
2061 $org_unit = $actor->request(
2062 'open-ils.actor.org_unit_list.search' => id => $org
2071 # vim: noet:ts=4:sw=4