1 package OpenILS::Application::SuperCat;
6 # All OpenSRF applications must be based on OpenSRF::Application or
7 # a subclass thereof. Makes sense, eh?
8 use OpenSRF::Application;
9 use base qw/OpenSRF::Application/;
11 # This is the client class, used for connecting to open-ils.storage
12 use OpenSRF::AppSession;
14 # This is an extention of Error.pm that supplies some error types to throw
15 use OpenSRF::EX qw(:try);
17 # This is a helper class for querying the OpenSRF Settings application ...
18 use OpenSRF::Utils::SettingsClient;
20 # ... and here we have the built in logging helper ...
21 use OpenSRF::Utils::Logger qw($logger);
23 # ... and this is our OpenILS object (en|de)coder and psuedo-ORM package.
24 use OpenILS::Utils::Fieldmapper;
27 # We'll be working with XML, so...
30 use Unicode::Normalize;
43 # we need an XML parser
44 $_parser = new XML::LibXML;
47 $_xslt = new XML::LibXSLT;
49 # parse the MODS xslt ...
50 my $mods3_xslt = $_parser->parse_file(
51 OpenSRF::Utils::SettingsClient
53 ->config_value( dirs => 'xsl' ).
54 "/MARC21slim2MODS3.xsl"
56 # and stash a transformer
57 $record_xslt{mods3}{xslt} = $_xslt->parse_stylesheet( $mods3_xslt );
58 $record_xslt{mods3}{namespace_uri} = 'http://www.loc.gov/mods/v3';
59 $record_xslt{mods3}{docs} = 'http://www.loc.gov/mods/';
60 $record_xslt{mods3}{schema_location} = 'http://www.loc.gov/standards/mods/v3/mods-3-1.xsd';
62 # parse the MODS xslt ...
63 my $mods_xslt = $_parser->parse_file(
64 OpenSRF::Utils::SettingsClient
66 ->config_value( dirs => 'xsl' ).
67 "/MARC21slim2MODS.xsl"
69 # and stash a transformer
70 $record_xslt{mods}{xslt} = $_xslt->parse_stylesheet( $mods_xslt );
71 $record_xslt{mods}{namespace_uri} = 'http://www.loc.gov/mods/';
72 $record_xslt{mods}{docs} = 'http://www.loc.gov/mods/';
73 $record_xslt{mods}{schema_location} = 'http://www.loc.gov/standards/mods/mods.xsd';
75 # parse the ATOM entry xslt ...
76 my $atom_xslt = $_parser->parse_file(
77 OpenSRF::Utils::SettingsClient
79 ->config_value( dirs => 'xsl' ).
80 "/MARC21slim2ATOM.xsl"
82 # and stash a transformer
83 $record_xslt{atom}{xslt} = $_xslt->parse_stylesheet( $atom_xslt );
84 $record_xslt{atom}{namespace_uri} = 'http://www.w3.org/2005/Atom';
85 $record_xslt{atom}{docs} = 'http://www.ietf.org/rfc/rfc4287.txt';
87 # parse the RDFDC xslt ...
88 my $rdf_dc_xslt = $_parser->parse_file(
89 OpenSRF::Utils::SettingsClient
91 ->config_value( dirs => 'xsl' ).
92 "/MARC21slim2RDFDC.xsl"
94 # and stash a transformer
95 $record_xslt{rdf_dc}{xslt} = $_xslt->parse_stylesheet( $rdf_dc_xslt );
96 $record_xslt{rdf_dc}{namespace_uri} = 'http://purl.org/dc/elements/1.1/';
97 $record_xslt{rdf_dc}{schema_location} = 'http://purl.org/dc/elements/1.1/';
99 # parse the SRWDC xslt ...
100 my $srw_dc_xslt = $_parser->parse_file(
101 OpenSRF::Utils::SettingsClient
103 ->config_value( dirs => 'xsl' ).
104 "/MARC21slim2SRWDC.xsl"
106 # and stash a transformer
107 $record_xslt{srw_dc}{xslt} = $_xslt->parse_stylesheet( $srw_dc_xslt );
108 $record_xslt{srw_dc}{namespace_uri} = 'info:srw/schema/1/dc-schema';
109 $record_xslt{srw_dc}{schema_location} = 'http://www.loc.gov/z3950/agency/zing/srw/dc-schema.xsd';
111 # parse the OAIDC xslt ...
112 my $oai_dc_xslt = $_parser->parse_file(
113 OpenSRF::Utils::SettingsClient
115 ->config_value( dirs => 'xsl' ).
116 "/MARC21slim2OAIDC.xsl"
118 # and stash a transformer
119 $record_xslt{oai_dc}{xslt} = $_xslt->parse_stylesheet( $oai_dc_xslt );
120 $record_xslt{oai_dc}{namespace_uri} = 'http://www.openarchives.org/OAI/2.0/oai_dc/';
121 $record_xslt{oai_dc}{schema_location} = 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd';
123 # parse the RSS xslt ...
124 my $rss_xslt = $_parser->parse_file(
125 OpenSRF::Utils::SettingsClient
127 ->config_value( dirs => 'xsl' ).
128 "/MARC21slim2RSS2.xsl"
130 # and stash a transformer
131 $record_xslt{rss2}{xslt} = $_xslt->parse_stylesheet( $rss_xslt );
133 register_record_transforms();
138 sub register_record_transforms {
139 for my $type ( keys %record_xslt ) {
140 __PACKAGE__->register_method(
141 method => 'retrieve_record_transform',
142 api_name => "open-ils.supercat.record.$type.retrieve",
146 { desc => "Returns the \U$type\E representation ".
147 "of the requested bibliographic record",
151 desc => 'An OpenILS biblio::record_entry id',
155 { desc => "The bib record in \U$type\E",
164 my $stuff = NFC(shift());
165 $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
169 sub new_record_holdings {
175 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
177 my $tree = $_storage->request(
178 "open-ils.cstore.direct.biblio.record_entry.retrieve",
180 {flesh => 3, flesh_fields => [qw/call_numbers copies location status owning_lib circ_lib/] }
183 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
187 my $xml = "<hold:volumes xmlns:hold='http://open-ils.org/spec/holdings/v1'>";
189 for my $cn (@{$tree->call_numbers}) {
192 next unless grep {$_->circ_lib->shortname =~ /^$ou/} @{$cn->copies};
195 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
196 $cn_class =~ s/Fieldmapper-//gso;
197 my $cn_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cn_class/".$cn->id, $month, $day);
199 my $cn_lib = $cn->owning_lib->shortname;
201 my $cn_label = $cn->label;
203 $xml .= "<hold:volume id='$cn_tag' lib='$cn_lib' label='$cn_label'><hold:copies>";
205 for my $cp (@{$cn->copies}) {
208 next unless $cp->circ_lib->shortname =~ /^$ou/;
211 (my $cp_class = $cp->class_name) =~ s/::/-/gso;
212 $cp_class =~ s/Fieldmapper-//gso;
213 my $cp_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cp_class/".$cp->id, $month, $day);
215 my $cp_stat = $cp->status->name;
217 my $cp_loc = $cp->location->name;
219 my $cp_lib = $cp->circ_lib->shortname;
221 my $cp_bc = $cp->barcode;
223 $xml .= "<hold:copy id='$cp_tag' barcode='$cp_bc'><hold:status>$cp_stat</hold:status><hold:location>$cp_loc</hold:location><hold:circlib>$cp_lib</hold:circlib><hold:notes>";
225 #for my $note ( @{$_storage->request( "open-ils.cstore.direct.asset.copy_note.search.atomic" => {owning_copy => $cp->id, pub => "t" })->gather(1)} ) {
226 # $xml .= sprintf('<hold:note date="%s" title="%s">%s</hold:note>',$note->create_date, escape($note->title), escape($note->value));
229 $xml .= "</hold:notes><hold:statcats>";
231 #for my $sce ( @{$_storage->request( "open-ils.cstore.direct.asset.stat_cat_entry_copy_map.search.atomic" => { owning_copy => $cp->id })->gather(1)} ) {
232 # my $sc = $holdings_data_cache{statcat}{$sce->stat_cat_entry};
233 # $xml .= sprintf('<hold:statcat>%s</hold:statcat>',escape($sc->value));
236 $xml .= "</hold:statcats></hold:copy>";
239 $xml .= "</hold:copies></hold:volume>";
242 $xml .= "</hold:volumes>";
246 __PACKAGE__->register_method(
247 method => 'new_record_holdings',
248 api_name => 'open-ils.supercat.record.holdings_xml.retrieve',
253 Returns the XML representation of the requested bibliographic record's holdings
258 desc => 'An OpenILS biblio::record_entry id',
262 { desc => 'The bib record holdings hierarchy in XML',
269 sub record_holdings {
274 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
276 if (!$holdings_data_cache{status}) {
277 $holdings_data_cache{status} = {
278 map { ($_->id => $_) } @{ $_storage->request( "open-ils.cstore.direct.config.copy_status.search.atomic", {id => {'<>' => undef}} )->gather(1) }
280 $holdings_data_cache{location} = {
281 map { ($_->id => $_) } @{ $_storage->request( "open-ils.cstore.direct.asset.copy_location.retrieve.all.atomic", {id => {'<>' => undef}} )->gather(1) }
283 $holdings_data_cache{ou} =
287 } @{$_storage->request( "open-ils.cstore.direct.actor.org_unit.search.atomic" => { id => { '<>' => undef } } )->gather(1)}
289 $holdings_data_cache{statcat} =
293 } @{$_storage->request( "open-ils.cstore.direct.asset.stat_cat_entry.search.atomic" => { id => { '<>' => undef } } )->gather(1)}
298 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
302 my $xml = "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>";
304 for my $cn ( @{$_storage->request( "open-ils.cstore.direct.asset.call_number.search.atomic" => {record => $bib} )->gather(1)} ) {
305 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
306 $cn_class =~ s/Fieldmapper-//gso;
307 my $cn_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cn_class/".$cn->id, $month, $day);
309 my $cn_lib = $holdings_data_cache{ou}{$cn->owning_lib}->shortname;
311 my $cn_label = $cn->label;
313 $xml .= "<volume id='$cn_tag' lib='$cn_lib' label='$cn_label'><copies>";
315 for my $cp ( @{$_storage->request( "open-ils.cstore.direct.asset.copy.search.atomic" => {call_number => $cn->id} )->gather(1)} ) {
316 (my $cp_class = $cn->class_name) =~ s/::/-/gso;
317 $cp_class =~ s/Fieldmapper-//gso;
318 my $cp_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cp_class/".$cp->id, $month, $day);
320 my $cp_stat = $holdings_data_cache{status}{$cp->status}->name;
322 my $cp_loc = $holdings_data_cache{location}{$cp->location}->name;
324 my $cp_lib = $holdings_data_cache{ou}{$cp->circ_lib}->shortname;
326 my $cp_bc = $cp->barcode;
328 $xml .= "<copy id='$cp_tag' barcode='$cp_bc'><status>$cp_stat</status><location>$cp_loc</location><circlib>$cp_lib</circlib><notes>";
330 for my $note ( @{$_storage->request( "open-ils.cstore.direct.asset.copy_note.search.atomic" => {id => $cp->id, pub => "t" })->gather(1)} ) {
331 $xml .= sprintf('<note date="%s" title="%s">%s</note>',$note->create_date, escape($note->title), escape($note->value));
334 $xml .= "</notes><statcats>";
336 for my $sce ( @{$_storage->request( "open-ils.cstore.direct.asset.stat_cat_entry_copy_map.search.atomic" => { owning_copy => $cp->id })->gather(1)} ) {
337 my $sc = $holdings_data_cache{statcat}{$sce->stat_cat_entry};
338 $xml .= sprintf('<statcat>%s</statcat>',escape($sc->value));
341 $xml .= "</statcats></copy>";
347 $xml .= "</volumes>";
354 $text =~ s/&/&/gsom;
355 $text =~ s/</</gsom;
356 $text =~ s/>/>/gsom;
357 $text =~ s/"/\\"/gsom;
368 my ($d,$m,$y) = (localtime)[4,5,6];
369 $when = sprintf('%4d-%02d-%02d', $y + 1900, $m + 1, $d);
373 $type = 'authority' if ($self->api_name =~ /authority/o);
375 my $axis = 'create_date';
376 $axis = 'edit_date' if ($self->api_name =~ /edit/o);
378 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
382 "open-ils.cstore.direct.$type.record_entry.id_list.atomic",
383 { $axis => { ">" => $when } },
384 { order_by => "$axis desc", limit => $limit } )
388 for my $t ( qw/biblio authority/ ) {
389 for my $a ( qw/import edit/ ) {
391 __PACKAGE__->register_method(
392 method => 'recent_changes',
393 api_name => "open-ils.supercat.$t.record.$a.recent",
397 { desc => "Returns a list of recently ${a}ed $t records",
401 desc => "Date to start looking for ${a}ed records",
406 desc => "Maximum count to retrieve",
410 { desc => "An id list of $t records",
418 sub retrieve_record_marcxml {
423 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
428 ->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $rid )
434 __PACKAGE__->register_method(
435 method => 'retrieve_record_marcxml',
436 api_name => 'open-ils.supercat.record.marcxml.retrieve',
441 Returns the MARCXML representation of the requested bibliographic record
446 desc => 'An OpenILS biblio::record_entry id',
450 { desc => 'The bib record in MARCXML',
455 sub retrieve_record_transform {
460 (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
462 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
465 warn "Fetching record entry $rid\n";
466 my $marc = $_storage->request(
467 'open-ils.cstore.direct.biblio.record_entry.retrieve',
470 warn "Fetched record entry $rid\n";
472 $_storage->disconnect;
474 return entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $marc ) )->toString);
478 sub retrieve_metarecord_mods {
483 my $_storage = OpenSRF::AppSession->connect( 'open-ils.cstore' );
485 # Get the metarecord in question
488 'open-ils.cstore.direct.metabib.metarecord.retrieve' => $rid
491 # Now get the map of all bib records for the metarecord
494 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
498 $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
500 # and retrieve the lead (master) record as MODS
502 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
503 ->run($mr->master_record);
504 my $master_mods = $_parser->parse_string($master)->documentElement;
505 $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
507 # ... and a MODS clone to populate, with guts removed.
508 my $mods = $_parser->parse_string($master)->documentElement;
509 $mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
510 ($mods) = $mods->findnodes('//mods:mods');
511 $mods->removeChildNodes;
513 # Add the metarecord ID as a (locally defined) info URI
514 my $recordInfo = $mods
516 ->createElement("mods:recordInfo");
518 my $recordIdentifier = $mods
520 ->createElement("mods:recordIdentifier");
522 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
527 $recordIdentifier->appendTextNode(
528 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
531 $recordInfo->appendChild($recordIdentifier);
532 $mods->appendChild($recordInfo);
534 # Grab the title, author and ISBN for the master record and populate the metarecord
535 my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
538 $title->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
539 $title = $mods->ownerDocument->importNode($title);
540 $mods->appendChild($title);
543 my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
545 $author->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
546 $author = $mods->ownerDocument->importNode($author);
547 $mods->appendChild($author);
550 my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
552 $isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
553 $isbn = $mods->ownerDocument->importNode($isbn);
554 $mods->appendChild($isbn);
557 # ... and loop over the constituent records
558 for my $map ( @$recs ) {
562 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
565 my $part_mods = $_parser->parse_string($rec);
566 $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
567 ($part_mods) = $part_mods->findnodes('//mods:mods');
569 for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
570 $node->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
571 $node = $mods->ownerDocument->importNode($node);
572 $mods->appendChild( $node );
575 my $relatedItem = $mods
577 ->createElement("mods:relatedItem");
579 $relatedItem->setAttribute( type => 'constituent' );
581 my $identifier = $mods
583 ->createElement("mods:identifier");
585 $identifier->setAttribute( type => 'uri' );
587 my $subRecordInfo = $mods
589 ->createElement("mods:recordInfo");
591 my $subRecordIdentifier = $mods
593 ->createElement("mods:recordIdentifier");
595 my $subid = $map->source;
596 $subRecordIdentifier->appendTextNode(
597 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
602 $subRecordInfo->appendChild($subRecordIdentifier);
604 $relatedItem->appendChild( $subRecordInfo );
606 my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
607 $tor->setNamespace( "http://www.loc.gov/mods/", "mods", 1 ) if ($tor);
608 $tor = $mods->ownerDocument->importNode($tor) if ($tor);
609 $relatedItem->appendChild($tor) if ($tor);
611 if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
612 $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
613 $part_isbn = $mods->ownerDocument->importNode($part_isbn);
614 $relatedItem->appendChild( $part_isbn );
617 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
621 $mods->appendChild( $relatedItem );
625 $_storage->disconnect;
627 return entityize($mods->toString);
630 __PACKAGE__->register_method(
631 method => 'retrieve_metarecord_mods',
632 api_name => 'open-ils.supercat.metarecord.mods.retrieve',
637 Returns the MODS representation of the requested metarecord
641 { name => 'metarecordId',
642 desc => 'An OpenILS metabib::metarecord id',
646 { desc => 'The metarecord in MODS',
651 sub list_metarecord_formats {
654 { namespace_uri => 'http://www.loc.gov/mods/',
655 docs => 'http://www.loc.gov/mods/',
656 schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
661 for my $type ( keys %metarecord_xslt ) {
664 { namespace_uri => $metarecord_xslt{$type}{namespace_uri},
665 docs => $metarecord_xslt{$type}{docs},
666 schema_location => $metarecord_xslt{$type}{schema_location},
673 __PACKAGE__->register_method(
674 method => 'list_metarecord_formats',
675 api_name => 'open-ils.supercat.metarecord.formats',
680 Returns the list of valid metarecord formats that supercat understands.
683 { desc => 'The format list',
689 sub list_record_formats {
692 { namespace_uri => 'http://www.loc.gov/MARC21/slim',
693 docs => 'http://www.loc.gov/marcxml/',
694 schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
699 for my $type ( keys %record_xslt ) {
702 { namespace_uri => $record_xslt{$type}{namespace_uri},
703 docs => $record_xslt{$type}{docs},
704 schema_location => $record_xslt{$type}{schema_location},
711 __PACKAGE__->register_method(
712 method => 'list_record_formats',
713 api_name => 'open-ils.supercat.record.formats',
718 Returns the list of valid record formats that supercat understands.
721 { desc => 'The format list',
732 throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
733 unless (length($isbn) >= 10);
735 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
737 # Create a storage session, since we'll be making muliple requests.
740 # Find the record that has that ISBN.
741 my $bibrec = $_storage->request(
742 'open-ils.cstore.direct.metabib.full_rec.search.atomic',
743 { tag => '020', subfield => 'a', value => { ilike => $isbn.'%'} }
746 # Go away if we don't have one.
747 return {} unless (@$bibrec);
749 # Find the metarecord for that bib record.
750 my $mr = $_storage->request(
751 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
752 {source => $bibrec->[0]->record}
755 # Find the other records for that metarecord.
756 my $records = $_storage->request(
757 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
758 {metarecord => $mr->[0]->metarecord}
761 # Just to be safe. There's currently no unique constraint on sources...
762 my %unique_recs = map { ($_->source, 1) } @$records;
763 my @rec_list = sort keys %unique_recs;
765 # And now fetch the ISBNs for thos records.
766 my $recs = $_storage->request(
767 'open-ils.cstore.direct.metabib.full_rec.search.atomic',
768 { tag => '020', subfield => 'a', record => \@rec_list }
771 # We're done with the storage server session.
772 $_storage->disconnect;
774 # Return the oISBN data structure. This will be XMLized at a higher layer.
776 { metarecord => $mr->[0]->metarecord,
777 record_list => { map { ($_->record, $_->value) } @$recs } };
780 __PACKAGE__->register_method(
782 api_name => 'open-ils.supercat.oisbn',
787 Returns the ISBN list for the metarecord of the requested isbn
792 desc => 'An ISBN. Duh.',
796 { desc => 'record to isbn map',