1 package OpenILS::WWW::SuperCat;
2 use strict; use warnings;
6 use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log);
7 use APR::Const -compile => qw(:error SUCCESS);
8 use Apache2::RequestRec ();
9 use Apache2::RequestIO ();
10 use Apache2::RequestUtil;
14 use OpenSRF::EX qw(:try);
15 use OpenSRF::Utils qw/:datetime/;
17 use OpenSRF::AppSession;
20 use Unicode::Normalize;
21 use OpenILS::Utils::Fieldmapper;
22 use OpenILS::WWW::SuperCat::Feed;
25 # set the bootstrap config when this module is loaded
26 my ($bootstrap, $supercat, $actor, $parser, $search);
35 OpenSRF::System->bootstrap_client( config_file => $bootstrap );
36 $supercat = OpenSRF::AppSession->create('open-ils.supercat');
37 $actor = OpenSRF::AppSession->create('open-ils.actor');
38 $search = OpenSRF::AppSession->create('open-ils.search');
39 $parser = new XML::LibXML;
45 return Apache2::Const::DECLINED if (-e $apache->filename);
47 (my $isbn = $apache->path_info) =~ s{^.*?([^/]+)$}{$1}o;
50 ->request("open-ils.supercat.oisbn", $isbn)
53 print "Content-type: application/xml; charset=utf-8\n\n";
54 print "<?xml version='1.0' encoding='UTF-8' ?>\n";
56 unless (exists $$list{metarecord}) {
58 return Apache2::Const::OK;
61 print "<idlist metarecord='$$list{metarecord}'>\n";
63 for ( keys %{ $$list{record_list} } ) {
64 (my $o = $$list{record_list}{$_}) =~s/^(\S+).*?$/$1/o;
65 print " <isbn record='$_'>$o</isbn>\n"
70 return Apache2::Const::OK;
76 return Apache2::Const::DECLINED if (-e $apache->filename);
79 my $rel_name = quotemeta($cgi->url(-relative=>1));
82 $add_path = 0 if ($cgi->url(-path_info=>1) =~ /$rel_name$/);
85 my $url = $cgi->url(-path_info=>$add_path);
86 my $root = (split 'unapi', $url)[0];
87 my $base = (split 'unapi', $url)[0] . 'unapi';
90 my $uri = $cgi->param('uri') || '';
91 my $host = $cgi->virtual_host || $cgi->server_name;
93 my $format = $cgi->param('format');
94 my ($id,$type,$command) = ('','','');
97 print "Content-type: application/xml; charset=utf-8\n";
99 if ($uri =~ m{^tag:[^:]+:([^\/]+)/(\d+)}o) {
102 $type = 'metarecord' if ($1 =~ /^m/o);
105 ->request("open-ils.supercat.$type.formats")
115 <type>text/html</type>
119 my ($type) = keys %$h;
120 $body .= "<format><name>$type</name><type>application/xml</type>";
122 for my $part ( qw/namespace_uri docs schema_location/ ) {
123 $body .= "<$part>$$h{$type}{$part}</$part>"
124 if ($$h{$type}{$part});
127 $body .= '</format>';
130 $body .= "</formats>\n";
132 $apache->custom_response( 300, $body);
136 ->request("open-ils.supercat.record.formats")
141 ->request("open-ils.supercat.metarecord.formats")
145 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
146 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
151 <type>text/html</type>
155 my ($type) = keys %$h;
156 print "<format><name>$type</name><type>application/xml</type>";
158 for my $part ( qw/namespace_uri docs schema_location/ ) {
159 print "<$part>$$h{$type}{$part}</$part>"
160 if ($$h{$type}{$part});
166 print "</formats>\n";
169 return Apache2::Const::OK;
174 if ($uri =~ m{^tag:[^:]+:([^\/]+)/(\d+)}o) {
177 $type = 'metarecord' if ($1 =~ /^m/o);
178 $command = 'retrieve';
181 if ($format eq 'opac') {
182 print "Location: $base/../../en-US/skin/default/xml/rresult.xml?m=$id\n\n"
183 if ($type eq 'metarecord');
184 print "Location: $base/../../en-US/skin/default/xml/rdetail.xml?r=$id\n\n"
185 if ($type eq 'record');
187 } elsif ($format =~ /^html/o) {
188 my $feed = create_record_feed(
194 $feed->creator($host);
195 $feed->update_ts(gmtime_ISO8601());
197 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
198 print entityize($feed->toString) . "\n";
200 return Apache2::Const::OK;
203 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
207 print "Content-type: text/html; charset=utf-8\n\n";
208 $apache->custom_response( 404, <<" HTML");
211 <title>$type $id not found!</title>
215 <center>Sorry, we couldn't $command a $type with the id of $id.</center>
222 print "Content-type: application/xml; charset=utf-8\n\n";
223 print $req->gather(1);
225 return Apache2::Const::OK;
231 return Apache2::Const::DECLINED if (-e $apache->filename);
235 my $rel_name = quotemeta($cgi->url(-relative=>1));
238 $add_path = 0 if ($cgi->url(-path_info=>1) =~ /$rel_name$/);
241 my $url = $cgi->url(-path_info=>$add_path);
242 my $root = (split 'supercat', $url)[0];
243 my $base = (split 'supercat', $url)[0] . 'supercat';
244 my $path = (split 'supercat', $url)[1];
245 my $unapi = (split 'supercat', $url)[0] . 'unapi';
247 my $host = $cgi->virtual_host || $cgi->server_name;
249 my ($id,$type,$format,$command) = reverse split '/', $path;
252 if ( $path =~ m{^/formats(?:/([^\/]+))?$}o ) {
253 print "Content-type: application/xml; charset=utf-8\n";
256 ->request("open-ils.supercat.$1.formats")
264 <type>text/html</type>
268 my ($type) = keys %$h;
269 print "<format><name>$type</name><type>application/xml</type>";
271 for my $part ( qw/namespace_uri docs schema_location/ ) {
272 print "<$part>$$h{$type}{$part}</$part>"
273 if ($$h{$type}{$part});
279 print "</formats>\n";
281 return Apache2::Const::OK;
285 ->request("open-ils.supercat.record.formats")
290 ->request("open-ils.supercat.metarecord.formats")
294 my %hash = map { ( (keys %$_)[0] => (values %$_)[0] ) } @$list;
295 $list = [ map { { $_ => $hash{$_} } } sort keys %hash ];
300 <type>text/html</type>
304 my ($type) = keys %$h;
305 print "<format><name>$type</name><type>application/xml</type>";
307 for my $part ( qw/namespace_uri docs schema_location/ ) {
308 print "<$part>$$h{$type}{$part}</$part>"
309 if ($$h{$type}{$part});
315 print "</formats>\n";
318 return Apache2::Const::OK;
321 if ($format eq 'opac') {
322 print "Location: $base/../../en-US/skin/default/xml/rresult.xml?m=$id\n\n"
323 if ($type eq 'metarecord');
324 print "Location: $base/../../en-US/skin/default/xml/rdetail.xml?r=$id\n\n"
325 if ($type eq 'record');
327 } elsif ($format =~ /^html/o) {
328 my $feed = create_record_feed( $format => [ $id ], $unapi,);
331 $feed->creator($host);
332 $feed->update_ts(gmtime_ISO8601());
334 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
335 print entityize($feed->toString) . "\n";
337 return Apache2::Const::OK;
340 my $req = $supercat->request("open-ils.supercat.$type.$format.$command",$id);
344 print "Content-type: text/html; charset=utf-8\n\n";
345 $apache->custom_response( 404, <<" HTML");
348 <title>$type $id not found!</title>
352 <center>Sorry, we couldn't $command a $type with the id of $id.</center>
359 print "Content-type: application/xml; charset=utf-8\n\n";
360 print $req->gather(1);
362 return Apache2::Const::OK;
368 return Apache2::Const::DECLINED if (-e $apache->filename);
372 my $year = (gmtime())[5] + 1900;
373 my $host = $cgi->virtual_host || $cgi->server_name;
375 my $rel_name = quotemeta($cgi->url(-relative=>1));
378 $add_path = 0 if ($cgi->url(-path_info=>1) =~ /$rel_name$/);
380 my $url = $cgi->url(-path_info=>$add_path);
381 my $root = (split 'feed', $url)[0];
382 my $base = (split 'bookbag', $url)[0] . 'bookbag';
383 my $path = (split 'bookbag', $url)[1];
384 my $unapi = (split 'feed', $url)[0] . 'unapi';
387 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
389 my ($id,$type) = reverse split '/', $path;
391 my $bucket = $actor->request("open-ils.actor.container.public.flesh", 'biblio', $id)->gather(1);
392 return Apache2::Const::NOT_FOUND unless($bucket);
394 my $bucket_tag = "tag:$host,$year:record_bucket/$id";
395 if ($type eq 'opac') {
396 print "Location: $root/../en-US/skin/default/xml/rresult.xml?rt=list&" .
397 join('&', map { "rl=" . $_->target_biblio_record_entry } @{ $bucket->items }) .
399 return Apache2::Const::OK;
402 my $feed = create_record_feed(
404 [ map { $_->target_biblio_record_entry } @{ $bucket->items } ],
409 $feed->title("Items in Book Bag [".$bucket->name."]");
410 $feed->creator($host);
411 $feed->update_ts(gmtime_ISO8601());
413 $feed->link(atom => $base . "/atom/$id" => 'application/atom+xml');
414 $feed->link(rss2 => $base . "/rss2/$id");
415 $feed->link(html => $base . "/html/$id" => 'text/html');
416 $feed->link(unapi => $unapi);
420 $root . '../en-US/skin/default/xml/rresult.xml?rt=list&' .
421 join('&', map { 'rl=' . $_->target_biblio_record_entry } @{$bucket->items} ),
426 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
427 print entityize($feed->toString) . "\n";
429 return Apache2::Const::OK;
438 if ($version eq '1.0') {
440 Content-type: application/opensearchdescription+xml; charset=utf-8
442 <?xml version="1.0" encoding="UTF-8"?>
443 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/">
444 <Url>$base/1.0/$lib/-/$class/{searchTerms}?startPage={startPage}&startIndex={startIndex}&count={count}</Url>
445 <Format>http://a9.com/-/spec/opensearchrss/1.0/</Format>
446 <ShortName>$lib</ShortName>
447 <LongName>Search $lib</LongName>
448 <Description>Search the $lib OPAC by $class.</Description>
449 <Tags>$lib book library</Tags>
450 <SampleSearch>harry+potter</SampleSearch>
451 <Developer>Mike Rylander for GPLS/PINES</Developer>
452 <Contact>feedback\@open-ils.org</Contact>
453 <SyndicationRight>open</SyndicationRight>
454 <AdultContent>false</AdultContent>
455 </OpenSearchDescription>
459 Content-type: application/opensearchdescription+xml; charset=utf-8
461 <?xml version="1.0" encoding="UTF-8"?>
462 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
463 <ShortName>$lib</ShortName>
464 <Description>Search the $lib OPAC by $class.</Description>
465 <Tags>$lib book library</Tags>
466 <Url type="application/atom+xml"
467 template="$base/1.1/$lib/atom/$class/{searchTerms}?startPage={startPage?}&startIndex={startIndex?}&count={count?}&language={language?}"/>
468 <Url type="application/x-rss+xml"
469 template="$base/1.1/$lib/rss2/$class/{searchTerms}?startPage={startPage?}&startIndex={startIndex?}&count={count?}&language={language?}"/>
470 <Url type="application/x-mods3+xml"
471 template="$base/1.1/$lib/mods3/$class/{searchTerms}?startPage={startPage?}&startIndex={startIndex?}&count={count?}&language={language?}"/>
472 <Url type="application/x-mods+xml"
473 template="$base/1.1/$lib/mods/$class/{searchTerms}?startPage={startPage?}&startIndex={startIndex?}&count={count?}&language={language?}"/>
474 <Url type="application/x-marcxml+xml"
475 template="$base/1.1/$lib/marcxml/$class/{searchTerms}?startPage={startPage?}&startIndex={startIndex?}&count={count?}&language={language?}"/>
476 <LongName>Search $lib</LongName>
477 <Query role="example" searchTerms="harry+potter" />
478 <Developer>Mike Rylander for GPLS/PINES</Developer>
479 <SyndicationRight>open</SyndicationRight>
480 <AdultContent>false</AdultContent>
481 <Language>en-US</Language>
482 <OutputEncoding>UTF-8</OutputEncoding>
483 <InputEncoding>UTF-8</InputEncoding>
484 </OpenSearchDescription>
488 return Apache2::Const::OK;
491 sub opensearch_feed {
493 return Apache2::Const::DECLINED if (-e $apache->filename);
496 my $year = (gmtime())[5] + 1900;
498 my $host = $cgi->virtual_host || $cgi->server_name;
500 my $rel_name = quotemeta($cgi->url(-relative=>1));
503 $add_path = 0 if ($cgi->url(-path_info=>1) =~ /$rel_name$/);
505 my $url = $cgi->url(-path_info=>$add_path);
506 my $root = (split 'opensearch', $url)[0];
507 my $base = (split 'opensearch', $url)[0] . 'opensearch';
508 my $unapi = (split 'opensearch', $url)[0] . 'unapi';
510 my $path = (split 'opensearch', $url)[1];
512 #warn "URL breakdown: $url ($rel_name) -> $root -> $base -> $path -> $unapi";
514 if ($path =~ m{^/?(1\.\d{1})/(?:([^/]+)/)?([^/]+)/osd.xml}o) {
521 $lib = $actor->request(
522 'open-ils.actor.org_unit_list.search' => parent_ou => undef
523 )->gather(1)->[0]->shortname;
530 return opensearch_osd($version, $lib, $class, $base);
534 my $page = $cgi->param('startPage') || 1;
535 my $offset = $cgi->param('startIndex') || 1;
536 my $limit = $cgi->param('count') || 10;
537 my $lang = $cgi->param('language') || 'en-US';
539 $page = 1 if ($page !~ /^\d+$/);
540 $offset = 1 if ($offset !~ /^\d+$/);
541 $limit = 10 if ($limit !~ /^\d+$/); $limit = 25 if ($limit > 25);
542 $lang = 'en-US' if ($lang =~ /^{/ or $lang eq '*');
545 $offset = ($page - 1) * $limit;
550 my (undef,$version,$org,$type,$class,$terms) = split '/', $path;
552 $terms ||= $cgi->param('searchTerms');
553 $class ||= $cgi->param('searchClass') || '-';
554 $type ||= $cgi->param('responseType') || '-';
555 $org ||= $cgi->param('searchOrg') || '-';
557 if ($version eq '1.0') {
559 } elsif ($type eq '-') {
564 $class = 'keyword' if ($class eq '-');
568 #warn "searching for $class -> [$terms] via OS $version, response type $type";
572 $org_unit = $actor->request(
573 'open-ils.actor.org_unit_list.search' => parent_ou => undef
576 $org_unit = $actor->request(
577 'open-ils.actor.org_unit_list.search' => shortname => $org
581 my $recs = $search->request(
582 'open-ils.search.biblio.multiclass' => {
583 searches => { $class => { term => $terms, }, },
584 org_unit => $org_unit->[0]->id,
590 my $feed = create_record_feed(
592 [ map { $_->[0] } @{$recs->{ids}} ],
597 $feed->search($terms);
599 $feed->title("Search results for [$class => $terms] at ".$org_unit->[0]->name);
600 $feed->creator($host);
601 $feed->update_ts(gmtime_ISO8601());
605 'http://a9.com/-/spec/opensearch/1.1/',
612 'http://a9.com/-/spec/opensearch/1.1/',
619 'http://a9.com/-/spec/opensearch/1.1/',
626 $base . "/$version/$org/$type/$class?searchTerms=$terms&startIndex=" . int($offset + $limit + 1) . "&count=" . $limit =>
627 'application/opensearch+xml'
628 ) if ($offset + $limit < $recs->{count});
632 $base . "/$version/$org/$type/$class?searchTerms=$terms&startIndex=" . int(($offset - $limit) + 1) . "&count=" . $limit =>
633 'application/opensearch+xml'
638 $base . "/$version/$org/$type/$class?searchTerms=$terms" =>
639 'application/opensearch+xml'
642 $feed->link( unapi => $unapi);
646 # $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
647 # join('&', map { 'rl=' . $_->[0] } @{$recs->{ids}} ),
653 $root . "../$lang/skin/default/xml/rresult.xml?rt=list&" .
654 join('&', map { 'rl=' . $_->[0] } @{$recs->{ids}} ),
658 print "Content-type: ". $feed->type ."; charset=utf-8\n\n";
659 print entityize($feed->toString) . "\n";
661 return Apache2::Const::OK;
664 sub create_record_feed {
670 my $base = $cgi->url;
671 my $host = $cgi->virtual_host || $cgi->server_name;
673 my $year = (gmtime())[5] + 1900;
675 my $feed = new OpenILS::WWW::SuperCat::Feed ($type);
677 $feed->unapi($unapi);
679 $type = 'atom' if ($type eq 'html');
680 $type = 'marcxml' if ($type eq 'htmlcard');
682 for my $rec (@$records) {
683 my $item_tag = "tag:$host,$year:biblio-record_entry/" . $rec;
686 my $xml = $supercat->request(
687 "open-ils.supercat.record.$type.retrieve",
691 my $node = $feed->add_item($xml);
693 $node->id($item_tag);
694 $node->link(alternate => $feed->unapi . "?uri=$item_tag&format=opac" => 'text/html');
695 $node->link(opac => $feed->unapi . "?uri=$item_tag&format=opac");
696 $node->link(unapi => $feed->unapi . "?uri=$item_tag");
697 $node->link('unapi-uri' => $item_tag);
704 my $stuff = NFC(shift());
705 $stuff =~ s/&(?!\S+;)/&/gso;
706 $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;