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 # and finally, a storage server session
135 register_record_transforms();
140 sub register_record_transforms {
141 for my $type ( keys %record_xslt ) {
142 __PACKAGE__->register_method(
143 method => 'retrieve_record_transform',
144 api_name => "open-ils.supercat.record.$type.retrieve",
148 { desc => "Returns the \U$type\E representation ".
149 "of the requested bibliographic record",
153 desc => 'An OpenILS biblio::record_entry id',
157 { desc => "The bib record in \U$type\E",
166 my $stuff = NFC(shift());
167 $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
171 sub record_holdings {
176 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
178 if (!$holdings_data_cache{status}) {
179 $holdings_data_cache{status} = { map { ($_->id => $_) } @{ $_storage->request( "open-ils.storage.direct.config.copy_status.retrieve.all.atomic" )->gather(1) } };
180 $holdings_data_cache{location} = { map { ($_->id => $_) } @{ $_storage->request( "open-ils.storage.direct.asset.copy_location.retrieve.all.atomic" )->gather(1) } };
181 $holdings_data_cache{ou} =
185 } @{$_storage->request( "open-ils.storage.direct.actor.org_unit.search_where.atomic" => { id => { '>' => 0 } } )->gather(1)}
187 $holdings_data_cache{statcat} =
191 } @{$_storage->request( "open-ils.storage.direct.asset.stat_cat_entry.search_where.atomic" => { id => { '>' => 0 } } )->gather(1)}
196 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
200 my $xml = "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>";
202 for my $cn ( @{$_storage->request( "open-ils.storage.direct.asset.call_number.search.record.atomic" => $bib )->gather(1)} ) {
203 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
204 $cn_class =~ s/Fieldmapper-//gso;
205 my $cn_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cn_class/".$cn->id, $month, $day);
207 my $cn_lib = $holdings_data_cache{ou}{$cn->owning_lib}->shortname;
209 my $cn_label = $cn->label;
211 $xml .= "<volume id='$cn_tag' lib='$cn_lib' label='$cn_label'><copies>";
213 for my $cp ( @{$_storage->request( "open-ils.storage.direct.asset.copy.search.call_number.atomic" => $cn->id )->gather(1)} ) {
214 (my $cp_class = $cn->class_name) =~ s/::/-/gso;
215 $cp_class =~ s/Fieldmapper-//gso;
216 my $cp_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cp_class/".$cp->id, $month, $day);
218 my $cp_stat = $holdings_data_cache{status}{$cp->status}->name;
220 my $cp_loc = $holdings_data_cache{location}{$cp->location}->name;
222 my $cp_lib = $holdings_data_cache{ou}{$cp->circ_lib}->shortname;
224 my $cp_bc = $cp->barcode;
226 $xml .= "<copy id='$cp_tag' barcode='$cp_bc'><status>$cp_stat</status><location>$cp_loc</location><circlib>$cp_lib</circlib><notes>";
228 for my $note ( @{$_storage->request( "open-ils.storage.direct.asset.copy_note.search.atomic" => {id => $cp->id, pub => "t" })->gather(1)} ) {
229 $xml .= sprintf('<note date="%s" title="%s">%s</note>',$note->create_date, escape($note->title), escape($note->value));
232 $xml .= "</notes><statcats>";
234 for my $sce ( @{$_storage->request( "open-ils.storage.direct.asset.stat_cat_entry_copy_map.search.atomic" => { owning_copy => $cp->id })->gather(1)} ) {
235 my $sc = $holdings_data_cache{statcat}{$sce->stat_cat_entry};
236 $xml .= sprintf('<statcat>%s</statcat>',escape($sc->value));
239 $xml .= "</statcats></copy>";
245 $xml .= "</volumes>";
249 __PACKAGE__->register_method(
250 method => 'record_holdings',
251 api_name => 'open-ils.supercat.record.holdings_xml.retrieve',
256 Returns the XML representation of the requested bibliographic record's holdings
261 desc => 'An OpenILS biblio::record_entry id',
265 { desc => 'The bib record holdings hierarchy in XML',
273 $text =~ s/&/&/gsom;
274 $text =~ s/</</gsom;
275 $text =~ s/>/>/gsom;
286 my ($d,$m,$y) = (localtime)[4,5,6];
287 $when = sprintf('%4d-%02d-%02d', $y + 1900, $m + 1, $d);
291 $type = 'authority' if ($self->api_name =~ /authority/o);
293 my $axis = 'create_date';
294 $axis = 'edit_date' if ($self->api_name =~ /edit/o);
296 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
300 "open-ils.storage.id_list.$type.record_entry.search_where.atomic",
301 { $axis => { ">" => $when } },
302 { order_by => "$axis desc", limit => $limit } )
306 for my $t ( qw/biblio authority/ ) {
307 for my $a ( qw/import edit/ ) {
309 __PACKAGE__->register_method(
310 method => 'recent_changes',
311 api_name => "open-ils.supercat.$t.record.$a.recent",
315 { desc => "Returns a list of recently ${a}ed $t records",
319 desc => "Date to start looking for ${a}ed records",
324 desc => "Maximum count to retrieve",
328 { desc => "An id list of $t records",
336 sub retrieve_record_marcxml {
341 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
346 ->request( 'open-ils.storage.direct.biblio.record_entry.retrieve' => $rid )
352 __PACKAGE__->register_method(
353 method => 'retrieve_record_marcxml',
354 api_name => 'open-ils.supercat.record.marcxml.retrieve',
359 Returns the MARCXML representation of the requested bibliographic record
364 desc => 'An OpenILS biblio::record_entry id',
368 { desc => 'The bib record in MARCXML',
373 sub retrieve_record_transform {
378 (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
380 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
382 warn "Fetching record entry $rid\n";
383 my $marc = $_storage->request(
384 'open-ils.storage.direct.biblio.record_entry.retrieve',
387 warn "Fetched record entry $rid\n";
389 return entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $marc ) )->toString);
393 sub retrieve_metarecord_mods {
398 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
403 # Get the metarecord in question
406 'open-ils.storage.direct.metabib.metarecord.retrieve' => $rid
409 # Now get the map of all bib records for the metarecord
412 'open-ils.storage.direct.metabib.metarecord_source_map.search.metarecord.atomic',
416 $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
418 # and retrieve the lead (master) record as MODS
420 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
421 ->run($mr->master_record);
422 my $master_mods = $_parser->parse_string($master)->documentElement;
423 $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
425 # ... and a MODS clone to populate, with guts removed.
426 my $mods = $_parser->parse_string($master)->documentElement;
427 $mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
428 ($mods) = $mods->findnodes('//mods:mods');
429 $mods->removeChildNodes;
431 # Add the metarecord ID as a (locally defined) info URI
432 my $recordInfo = $mods
434 ->createElement("mods:recordInfo");
436 my $recordIdentifier = $mods
438 ->createElement("mods:recordIdentifier");
440 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
445 $recordIdentifier->appendTextNode(
446 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
449 $recordInfo->appendChild($recordIdentifier);
450 $mods->appendChild($recordInfo);
452 # Grab the title, author and ISBN for the master record and populate the metarecord
453 my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
456 $title->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
457 $title = $mods->ownerDocument->importNode($title);
458 $mods->appendChild($title);
461 my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
463 $author->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
464 $author = $mods->ownerDocument->importNode($author);
465 $mods->appendChild($author);
468 my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
470 $isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
471 $isbn = $mods->ownerDocument->importNode($isbn);
472 $mods->appendChild($isbn);
475 # ... and loop over the constituent records
476 for my $map ( @$recs ) {
480 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
483 my $part_mods = $_parser->parse_string($rec);
484 $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
485 ($part_mods) = $part_mods->findnodes('//mods:mods');
487 for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
488 $node->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
489 $node = $mods->ownerDocument->importNode($node);
490 $mods->appendChild( $node );
493 my $relatedItem = $mods
495 ->createElement("mods:relatedItem");
497 $relatedItem->setAttribute( type => 'constituent' );
499 my $identifier = $mods
501 ->createElement("mods:identifier");
503 $identifier->setAttribute( type => 'uri' );
505 my $subRecordInfo = $mods
507 ->createElement("mods:recordInfo");
509 my $subRecordIdentifier = $mods
511 ->createElement("mods:recordIdentifier");
513 my $subid = $map->source;
514 $subRecordIdentifier->appendTextNode(
515 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
520 $subRecordInfo->appendChild($subRecordIdentifier);
522 $relatedItem->appendChild( $subRecordInfo );
524 my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
525 $tor->setNamespace( "http://www.loc.gov/mods/", "mods", 1 ) if ($tor);
526 $tor = $mods->ownerDocument->importNode($tor) if ($tor);
527 $relatedItem->appendChild($tor) if ($tor);
529 if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
530 $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
531 $part_isbn = $mods->ownerDocument->importNode($part_isbn);
532 $relatedItem->appendChild( $part_isbn );
535 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
539 $mods->appendChild( $relatedItem );
543 $_storage->disconnect;
545 return entityize($mods->toString);
548 __PACKAGE__->register_method(
549 method => 'retrieve_metarecord_mods',
550 api_name => 'open-ils.supercat.metarecord.mods.retrieve',
555 Returns the MODS representation of the requested metarecord
559 { name => 'metarecordId',
560 desc => 'An OpenILS metabib::metarecord id',
564 { desc => 'The metarecord in MODS',
569 sub list_metarecord_formats {
572 { namespace_uri => 'http://www.loc.gov/mods/',
573 docs => 'http://www.loc.gov/mods/',
574 schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
579 for my $type ( keys %metarecord_xslt ) {
582 { namespace_uri => $metarecord_xslt{$type}{namespace_uri},
583 docs => $metarecord_xslt{$type}{docs},
584 schema_location => $metarecord_xslt{$type}{schema_location},
591 __PACKAGE__->register_method(
592 method => 'list_metarecord_formats',
593 api_name => 'open-ils.supercat.metarecord.formats',
598 Returns the list of valid metarecord formats that supercat understands.
601 { desc => 'The format list',
607 sub list_record_formats {
610 { namespace_uri => 'http://www.loc.gov/MARC21/slim',
611 docs => 'http://www.loc.gov/marcxml/',
612 schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
617 for my $type ( keys %record_xslt ) {
620 { namespace_uri => $record_xslt{$type}{namespace_uri},
621 docs => $record_xslt{$type}{docs},
622 schema_location => $record_xslt{$type}{schema_location},
629 __PACKAGE__->register_method(
630 method => 'list_record_formats',
631 api_name => 'open-ils.supercat.record.formats',
636 Returns the list of valid record formats that supercat understands.
639 { desc => 'The format list',
650 throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
651 unless (length($isbn) >= 10);
653 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
655 # Create a storage session, since we'll be making muliple requests.
658 # Find the record that has that ISBN.
659 my $bibrec = $_storage->request(
660 'open-ils.storage.direct.metabib.full_rec.search_where.atomic',
661 { tag => '020', subfield => 'a', value => { ilike => $isbn.'%'} }
664 # Go away if we don't have one.
665 return {} unless (@$bibrec);
667 # Find the metarecord for that bib record.
668 my $mr = $_storage->request(
669 'open-ils.storage.direct.metabib.metarecord_source_map.search.source.atomic',
673 # Find the other records for that metarecord.
674 my $records = $_storage->request(
675 'open-ils.storage.direct.metabib.metarecord_source_map.search.metarecord.atomic',
679 # Just to be safe. There's currently no unique constraint on sources...
680 my %unique_recs = map { ($_->source, 1) } @$records;
681 my @rec_list = sort keys %unique_recs;
683 # And now fetch the ISBNs for thos records.
684 my $recs = $_storage->request(
685 'open-ils.storage.direct.metabib.full_rec.search_where.atomic',
686 { tag => '020', subfield => 'a', record => \@rec_list }
689 # We're done with the storage server session.
690 $_storage->disconnect;
692 # Return the oISBN data structure. This will be XMLized at a higher layer.
694 { metarecord => $mr->[0]->metarecord,
695 record_list => { map { ($_->record, $_->value) } @$recs } };
698 __PACKAGE__->register_method(
700 api_name => 'open-ils.supercat.oisbn',
705 Returns the ISBN list for the metarecord of the requested isbn
710 desc => 'An ISBN. Duh.',
714 { desc => 'record to isbn map',