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",
182 bre => [qw/call_numbers/],
183 acn => [qw/copies owning_lib/],
184 acp => [qw/location status circ_lib stat_cat_entries notes/],
185 asce => [qw/stat_cat/],
190 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
194 my $xml = "<hold:volumes xmlns:hold='http://open-ils.org/spec/holdings/v1'>";
196 for my $cn (@{$tree->call_numbers}) {
199 next unless grep {$_->circ_lib->shortname =~ /^$ou/} @{$cn->copies};
202 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
203 $cn_class =~ s/Fieldmapper-//gso;
204 my $cn_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cn_class/".$cn->id, $month, $day);
206 my $cn_lib = $cn->owning_lib->shortname;
208 my $cn_label = $cn->label;
210 $xml .= "<hold:volume id='$cn_tag' lib='$cn_lib' label='$cn_label'><hold:copies>";
212 for my $cp (@{$cn->copies}) {
215 next unless $cp->circ_lib->shortname =~ /^$ou/;
218 (my $cp_class = $cp->class_name) =~ s/::/-/gso;
219 $cp_class =~ s/Fieldmapper-//gso;
220 my $cp_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cp_class/".$cp->id, $month, $day);
222 my $cp_stat = $cp->status->name;
224 my $cp_loc = $cp->location->name;
226 my $cp_lib = $cp->circ_lib->shortname;
228 my $cp_bc = $cp->barcode;
230 $xml .= "<hold:copy id='$cp_tag' barcode='$cp_bc'><hold:status>$cp_stat</hold:status>".
231 "<hold:location>$cp_loc</hold:location><hold:circlib>$cp_lib</hold:circlib><hold:notes>";
234 # for my $note ( @{$cp->notes} ) {
235 # next unless ( $sce->stat_cat->pub eq 't' );
236 # $xml .= sprintf('<hold:note date="%s" title="%s">%s</hold:note>',$note->create_date, escape($note->title), escape($note->value));
240 $xml .= "</hold:notes><hold:statcats>";
242 if ($cp->stat_cat_entries) {
243 for my $sce ( @{$cp->stat_cat_entries} ) {
244 next unless ( $sce->stat_cat->opac_visible eq 't' );
245 $xml .= sprintf('<hold:statcat name="%s">%s</hold:statcat>',escape($sce->stat_cat->name) ,escape($sce->value));
249 $xml .= "</hold:statcats></hold:copy>";
252 $xml .= "</hold:copies></hold:volume>";
255 $xml .= "</hold:volumes>";
259 __PACKAGE__->register_method(
260 method => 'new_record_holdings',
261 api_name => 'open-ils.supercat.record.holdings_xml.retrieve',
266 Returns the XML representation of the requested bibliographic record's holdings
271 desc => 'An OpenILS biblio::record_entry id',
275 { desc => 'The bib record holdings hierarchy in XML',
282 sub record_holdings {
287 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
289 if (!$holdings_data_cache{status}) {
290 $holdings_data_cache{status} = {
291 map { ($_->id => $_) } @{ $_storage->request( "open-ils.cstore.direct.config.copy_status.search.atomic", {id => {'<>' => undef}} )->gather(1) }
293 $holdings_data_cache{location} = {
294 map { ($_->id => $_) } @{ $_storage->request( "open-ils.cstore.direct.asset.copy_location.retrieve.all.atomic", {id => {'<>' => undef}} )->gather(1) }
296 $holdings_data_cache{ou} =
300 } @{$_storage->request( "open-ils.cstore.direct.actor.org_unit.search.atomic" => { id => { '<>' => undef } } )->gather(1)}
302 $holdings_data_cache{statcat} =
306 } @{$_storage->request( "open-ils.cstore.direct.asset.stat_cat_entry.search.atomic" => { id => { '<>' => undef } } )->gather(1)}
311 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
315 my $xml = "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>";
317 for my $cn ( @{$_storage->request( "open-ils.cstore.direct.asset.call_number.search.atomic" => {record => $bib} )->gather(1)} ) {
318 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
319 $cn_class =~ s/Fieldmapper-//gso;
320 my $cn_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cn_class/".$cn->id, $month, $day);
322 my $cn_lib = $holdings_data_cache{ou}{$cn->owning_lib}->shortname;
324 my $cn_label = $cn->label;
326 $xml .= "<volume id='$cn_tag' lib='$cn_lib' label='$cn_label'><copies>";
328 for my $cp ( @{$_storage->request( "open-ils.cstore.direct.asset.copy.search.atomic" => {call_number => $cn->id} )->gather(1)} ) {
329 (my $cp_class = $cn->class_name) =~ s/::/-/gso;
330 $cp_class =~ s/Fieldmapper-//gso;
331 my $cp_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cp_class/".$cp->id, $month, $day);
333 my $cp_stat = $holdings_data_cache{status}{$cp->status}->name;
335 my $cp_loc = $holdings_data_cache{location}{$cp->location}->name;
337 my $cp_lib = $holdings_data_cache{ou}{$cp->circ_lib}->shortname;
339 my $cp_bc = $cp->barcode;
341 $xml .= "<copy id='$cp_tag' barcode='$cp_bc'><status>$cp_stat</status><location>$cp_loc</location><circlib>$cp_lib</circlib><notes>";
343 for my $note ( @{$_storage->request( "open-ils.cstore.direct.asset.copy_note.search.atomic" => {id => $cp->id, pub => "t" })->gather(1)} ) {
344 $xml .= sprintf('<note date="%s" title="%s">%s</note>',$note->create_date, escape($note->title), escape($note->value));
347 $xml .= "</notes><statcats>";
349 for my $sce ( @{$_storage->request( "open-ils.cstore.direct.asset.stat_cat_entry_copy_map.search.atomic" => { owning_copy => $cp->id })->gather(1)} ) {
350 my $sc = $holdings_data_cache{statcat}{$sce->stat_cat_entry};
351 $xml .= sprintf('<statcat>%s</statcat>',escape($sc->value));
354 $xml .= "</statcats></copy>";
360 $xml .= "</volumes>";
367 $text =~ s/&/&/gsom;
368 $text =~ s/</</gsom;
369 $text =~ s/>/>/gsom;
370 $text =~ s/"/\\"/gsom;
381 my ($d,$m,$y) = (localtime)[4,5,6];
382 $when = sprintf('%4d-%02d-%02d', $y + 1900, $m + 1, $d);
386 $type = 'authority' if ($self->api_name =~ /authority/o);
388 my $axis = 'create_date';
389 $axis = 'edit_date' if ($self->api_name =~ /edit/o);
391 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
395 "open-ils.cstore.direct.$type.record_entry.id_list.atomic",
396 { $axis => { ">" => $when } },
397 { order_by => "$axis desc", limit => $limit } )
401 for my $t ( qw/biblio authority/ ) {
402 for my $a ( qw/import edit/ ) {
404 __PACKAGE__->register_method(
405 method => 'recent_changes',
406 api_name => "open-ils.supercat.$t.record.$a.recent",
410 { desc => "Returns a list of recently ${a}ed $t records",
414 desc => "Date to start looking for ${a}ed records",
419 desc => "Maximum count to retrieve",
423 { desc => "An id list of $t records",
431 sub retrieve_record_marcxml {
436 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
441 ->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $rid )
447 __PACKAGE__->register_method(
448 method => 'retrieve_record_marcxml',
449 api_name => 'open-ils.supercat.record.marcxml.retrieve',
454 Returns the MARCXML representation of the requested bibliographic record
459 desc => 'An OpenILS biblio::record_entry id',
463 { desc => 'The bib record in MARCXML',
468 sub retrieve_record_transform {
473 (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
475 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
478 warn "Fetching record entry $rid\n";
479 my $marc = $_storage->request(
480 'open-ils.cstore.direct.biblio.record_entry.retrieve',
483 warn "Fetched record entry $rid\n";
485 $_storage->disconnect;
487 return entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $marc ) )->toString);
491 sub retrieve_metarecord_mods {
496 my $_storage = OpenSRF::AppSession->connect( 'open-ils.cstore' );
498 # Get the metarecord in question
501 'open-ils.cstore.direct.metabib.metarecord.retrieve' => $rid
504 # Now get the map of all bib records for the metarecord
507 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
511 $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
513 # and retrieve the lead (master) record as MODS
515 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
516 ->run($mr->master_record);
517 my $master_mods = $_parser->parse_string($master)->documentElement;
518 $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
520 # ... and a MODS clone to populate, with guts removed.
521 my $mods = $_parser->parse_string($master)->documentElement;
522 $mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
523 ($mods) = $mods->findnodes('//mods:mods');
524 $mods->removeChildNodes;
526 # Add the metarecord ID as a (locally defined) info URI
527 my $recordInfo = $mods
529 ->createElement("mods:recordInfo");
531 my $recordIdentifier = $mods
533 ->createElement("mods:recordIdentifier");
535 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
540 $recordIdentifier->appendTextNode(
541 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
544 $recordInfo->appendChild($recordIdentifier);
545 $mods->appendChild($recordInfo);
547 # Grab the title, author and ISBN for the master record and populate the metarecord
548 my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
551 $title->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
552 $title = $mods->ownerDocument->importNode($title);
553 $mods->appendChild($title);
556 my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
558 $author->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
559 $author = $mods->ownerDocument->importNode($author);
560 $mods->appendChild($author);
563 my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
565 $isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
566 $isbn = $mods->ownerDocument->importNode($isbn);
567 $mods->appendChild($isbn);
570 # ... and loop over the constituent records
571 for my $map ( @$recs ) {
575 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
578 my $part_mods = $_parser->parse_string($rec);
579 $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
580 ($part_mods) = $part_mods->findnodes('//mods:mods');
582 for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
583 $node->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
584 $node = $mods->ownerDocument->importNode($node);
585 $mods->appendChild( $node );
588 my $relatedItem = $mods
590 ->createElement("mods:relatedItem");
592 $relatedItem->setAttribute( type => 'constituent' );
594 my $identifier = $mods
596 ->createElement("mods:identifier");
598 $identifier->setAttribute( type => 'uri' );
600 my $subRecordInfo = $mods
602 ->createElement("mods:recordInfo");
604 my $subRecordIdentifier = $mods
606 ->createElement("mods:recordIdentifier");
608 my $subid = $map->source;
609 $subRecordIdentifier->appendTextNode(
610 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
615 $subRecordInfo->appendChild($subRecordIdentifier);
617 $relatedItem->appendChild( $subRecordInfo );
619 my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
620 $tor->setNamespace( "http://www.loc.gov/mods/", "mods", 1 ) if ($tor);
621 $tor = $mods->ownerDocument->importNode($tor) if ($tor);
622 $relatedItem->appendChild($tor) if ($tor);
624 if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
625 $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
626 $part_isbn = $mods->ownerDocument->importNode($part_isbn);
627 $relatedItem->appendChild( $part_isbn );
630 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
634 $mods->appendChild( $relatedItem );
638 $_storage->disconnect;
640 return entityize($mods->toString);
643 __PACKAGE__->register_method(
644 method => 'retrieve_metarecord_mods',
645 api_name => 'open-ils.supercat.metarecord.mods.retrieve',
650 Returns the MODS representation of the requested metarecord
654 { name => 'metarecordId',
655 desc => 'An OpenILS metabib::metarecord id',
659 { desc => 'The metarecord in MODS',
664 sub list_metarecord_formats {
667 { namespace_uri => 'http://www.loc.gov/mods/',
668 docs => 'http://www.loc.gov/mods/',
669 schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
674 for my $type ( keys %metarecord_xslt ) {
677 { namespace_uri => $metarecord_xslt{$type}{namespace_uri},
678 docs => $metarecord_xslt{$type}{docs},
679 schema_location => $metarecord_xslt{$type}{schema_location},
686 __PACKAGE__->register_method(
687 method => 'list_metarecord_formats',
688 api_name => 'open-ils.supercat.metarecord.formats',
693 Returns the list of valid metarecord formats that supercat understands.
696 { desc => 'The format list',
702 sub list_record_formats {
705 { namespace_uri => 'http://www.loc.gov/MARC21/slim',
706 docs => 'http://www.loc.gov/marcxml/',
707 schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
712 for my $type ( keys %record_xslt ) {
715 { namespace_uri => $record_xslt{$type}{namespace_uri},
716 docs => $record_xslt{$type}{docs},
717 schema_location => $record_xslt{$type}{schema_location},
724 __PACKAGE__->register_method(
725 method => 'list_record_formats',
726 api_name => 'open-ils.supercat.record.formats',
731 Returns the list of valid record formats that supercat understands.
734 { desc => 'The format list',
745 throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
746 unless (length($isbn) >= 10);
748 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
750 # Create a storage session, since we'll be making muliple requests.
753 # Find the record that has that ISBN.
754 my $bibrec = $_storage->request(
755 'open-ils.cstore.direct.metabib.full_rec.search.atomic',
756 { tag => '020', subfield => 'a', value => { ilike => $isbn.'%'} }
759 # Go away if we don't have one.
760 return {} unless (@$bibrec);
762 # Find the metarecord for that bib record.
763 my $mr = $_storage->request(
764 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
765 {source => $bibrec->[0]->record}
768 # Find the other records for that metarecord.
769 my $records = $_storage->request(
770 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
771 {metarecord => $mr->[0]->metarecord}
774 # Just to be safe. There's currently no unique constraint on sources...
775 my %unique_recs = map { ($_->source, 1) } @$records;
776 my @rec_list = sort keys %unique_recs;
778 # And now fetch the ISBNs for thos records.
779 my $recs = $_storage->request(
780 'open-ils.cstore.direct.metabib.full_rec.search.atomic',
781 { tag => '020', subfield => 'a', record => \@rec_list }
784 # We're done with the storage server session.
785 $_storage->disconnect;
787 # Return the oISBN data structure. This will be XMLized at a higher layer.
789 { metarecord => $mr->[0]->metarecord,
790 record_list => { map { ($_->record, $_->value) } @$recs } };
793 __PACKAGE__->register_method(
795 api_name => 'open-ils.supercat.oisbn',
800 Returns the ISBN list for the metarecord of the requested isbn
805 desc => 'An ISBN. Duh.',
809 { desc => 'record to isbn map',