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 new_record_holdings {
177 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
178 my $_cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
180 my $tree = $_cstore->request(
181 "open-ils.cstore.direct.biblio.record_entry.retrieve",
183 {flesh => 3, flesh_fields => [qw/call_numbers copies location status owning_lib circ_lib/] }
186 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
190 my $xml = "<hold:volumes xmlns:hold='http://open-ils.org/spec/holdings/v1'>";
192 for my $cn (@{$tree->call_numbers}) {
195 next unless grep {$_->circ_lib->shortname =~ /^$ou/} @{$cn->copies};
198 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
199 $cn_class =~ s/Fieldmapper-//gso;
200 my $cn_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cn_class/".$cn->id, $month, $day);
202 my $cn_lib = $cn->owning_lib->shortname;
204 my $cn_label = $cn->label;
206 $xml .= "<hold:volume id='$cn_tag' lib='$cn_lib' label='$cn_label'><hold:copies>";
208 for my $cp (@{$cn->copies}) {
211 next unless $cp->circ_lib->shortname =~ /^$ou/;
214 (my $cp_class = $cp->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 = $cp->status->name;
220 my $cp_loc = $cp->location->name;
222 my $cp_lib = $cp->circ_lib->shortname;
224 my $cp_bc = $cp->barcode;
226 $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>";
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('<hold:note date="%s" title="%s">%s</hold:note>',$note->create_date, escape($note->title), escape($note->value));
232 $xml .= "</hold:notes><hold: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('<hold:statcat>%s</hold:statcat>',escape($sc->value));
239 $xml .= "</hold:statcats></hold:copy>";
242 $xml .= "</hold:copies></hold:volume>";
245 $xml .= "</hold:volumes>";
249 __PACKAGE__->register_method(
250 method => 'new_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',
272 sub record_holdings {
277 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
279 if (!$holdings_data_cache{status}) {
280 $holdings_data_cache{status} = { map { ($_->id => $_) } @{ $_storage->request( "open-ils.storage.direct.config.copy_status.retrieve.all.atomic" )->gather(1) } };
281 $holdings_data_cache{location} = { map { ($_->id => $_) } @{ $_storage->request( "open-ils.storage.direct.asset.copy_location.retrieve.all.atomic" )->gather(1) } };
282 $holdings_data_cache{ou} =
286 } @{$_storage->request( "open-ils.storage.direct.actor.org_unit.search_where.atomic" => { id => { '>' => 0 } } )->gather(1)}
288 $holdings_data_cache{statcat} =
292 } @{$_storage->request( "open-ils.storage.direct.asset.stat_cat_entry.search_where.atomic" => { id => { '>' => 0 } } )->gather(1)}
297 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
301 my $xml = "<volumes xmlns='http://open-ils.org/spec/holdings/v1'>";
303 for my $cn ( @{$_storage->request( "open-ils.storage.direct.asset.call_number.search.record.atomic" => $bib )->gather(1)} ) {
304 (my $cn_class = $cn->class_name) =~ s/::/-/gso;
305 $cn_class =~ s/Fieldmapper-//gso;
306 my $cn_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cn_class/".$cn->id, $month, $day);
308 my $cn_lib = $holdings_data_cache{ou}{$cn->owning_lib}->shortname;
310 my $cn_label = $cn->label;
312 $xml .= "<volume id='$cn_tag' lib='$cn_lib' label='$cn_label'><copies>";
314 for my $cp ( @{$_storage->request( "open-ils.storage.direct.asset.copy.search.call_number.atomic" => $cn->id )->gather(1)} ) {
315 (my $cp_class = $cn->class_name) =~ s/::/-/gso;
316 $cp_class =~ s/Fieldmapper-//gso;
317 my $cp_tag = sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:$cp_class/".$cp->id, $month, $day);
319 my $cp_stat = $holdings_data_cache{status}{$cp->status}->name;
321 my $cp_loc = $holdings_data_cache{location}{$cp->location}->name;
323 my $cp_lib = $holdings_data_cache{ou}{$cp->circ_lib}->shortname;
325 my $cp_bc = $cp->barcode;
327 $xml .= "<copy id='$cp_tag' barcode='$cp_bc'><status>$cp_stat</status><location>$cp_loc</location><circlib>$cp_lib</circlib><notes>";
329 for my $note ( @{$_storage->request( "open-ils.storage.direct.asset.copy_note.search.atomic" => {id => $cp->id, pub => "t" })->gather(1)} ) {
330 $xml .= sprintf('<note date="%s" title="%s">%s</note>',$note->create_date, escape($note->title), escape($note->value));
333 $xml .= "</notes><statcats>";
335 for my $sce ( @{$_storage->request( "open-ils.storage.direct.asset.stat_cat_entry_copy_map.search.atomic" => { owning_copy => $cp->id })->gather(1)} ) {
336 my $sc = $holdings_data_cache{statcat}{$sce->stat_cat_entry};
337 $xml .= sprintf('<statcat>%s</statcat>',escape($sc->value));
340 $xml .= "</statcats></copy>";
346 $xml .= "</volumes>";
353 $text =~ s/&/&/gsom;
354 $text =~ s/</</gsom;
355 $text =~ s/>/>/gsom;
366 my ($d,$m,$y) = (localtime)[4,5,6];
367 $when = sprintf('%4d-%02d-%02d', $y + 1900, $m + 1, $d);
371 $type = 'authority' if ($self->api_name =~ /authority/o);
373 my $axis = 'create_date';
374 $axis = 'edit_date' if ($self->api_name =~ /edit/o);
376 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
380 "open-ils.storage.id_list.$type.record_entry.search_where.atomic",
381 { $axis => { ">" => $when } },
382 { order_by => "$axis desc", limit => $limit } )
386 for my $t ( qw/biblio authority/ ) {
387 for my $a ( qw/import edit/ ) {
389 __PACKAGE__->register_method(
390 method => 'recent_changes',
391 api_name => "open-ils.supercat.$t.record.$a.recent",
395 { desc => "Returns a list of recently ${a}ed $t records",
399 desc => "Date to start looking for ${a}ed records",
404 desc => "Maximum count to retrieve",
408 { desc => "An id list of $t records",
416 sub retrieve_record_marcxml {
421 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
426 ->request( 'open-ils.storage.direct.biblio.record_entry.retrieve' => $rid )
432 __PACKAGE__->register_method(
433 method => 'retrieve_record_marcxml',
434 api_name => 'open-ils.supercat.record.marcxml.retrieve',
439 Returns the MARCXML representation of the requested bibliographic record
444 desc => 'An OpenILS biblio::record_entry id',
448 { desc => 'The bib record in MARCXML',
453 sub retrieve_record_transform {
458 (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
460 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
462 warn "Fetching record entry $rid\n";
463 my $marc = $_storage->request(
464 'open-ils.storage.direct.biblio.record_entry.retrieve',
467 warn "Fetched record entry $rid\n";
469 return entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $marc ) )->toString);
473 sub retrieve_metarecord_mods {
478 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
483 # Get the metarecord in question
486 'open-ils.storage.direct.metabib.metarecord.retrieve' => $rid
489 # Now get the map of all bib records for the metarecord
492 'open-ils.storage.direct.metabib.metarecord_source_map.search.metarecord.atomic',
496 $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
498 # and retrieve the lead (master) record as MODS
500 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
501 ->run($mr->master_record);
502 my $master_mods = $_parser->parse_string($master)->documentElement;
503 $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
505 # ... and a MODS clone to populate, with guts removed.
506 my $mods = $_parser->parse_string($master)->documentElement;
507 $mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
508 ($mods) = $mods->findnodes('//mods:mods');
509 $mods->removeChildNodes;
511 # Add the metarecord ID as a (locally defined) info URI
512 my $recordInfo = $mods
514 ->createElement("mods:recordInfo");
516 my $recordIdentifier = $mods
518 ->createElement("mods:recordIdentifier");
520 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
525 $recordIdentifier->appendTextNode(
526 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
529 $recordInfo->appendChild($recordIdentifier);
530 $mods->appendChild($recordInfo);
532 # Grab the title, author and ISBN for the master record and populate the metarecord
533 my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
536 $title->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
537 $title = $mods->ownerDocument->importNode($title);
538 $mods->appendChild($title);
541 my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
543 $author->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
544 $author = $mods->ownerDocument->importNode($author);
545 $mods->appendChild($author);
548 my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
550 $isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
551 $isbn = $mods->ownerDocument->importNode($isbn);
552 $mods->appendChild($isbn);
555 # ... and loop over the constituent records
556 for my $map ( @$recs ) {
560 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
563 my $part_mods = $_parser->parse_string($rec);
564 $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
565 ($part_mods) = $part_mods->findnodes('//mods:mods');
567 for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
568 $node->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
569 $node = $mods->ownerDocument->importNode($node);
570 $mods->appendChild( $node );
573 my $relatedItem = $mods
575 ->createElement("mods:relatedItem");
577 $relatedItem->setAttribute( type => 'constituent' );
579 my $identifier = $mods
581 ->createElement("mods:identifier");
583 $identifier->setAttribute( type => 'uri' );
585 my $subRecordInfo = $mods
587 ->createElement("mods:recordInfo");
589 my $subRecordIdentifier = $mods
591 ->createElement("mods:recordIdentifier");
593 my $subid = $map->source;
594 $subRecordIdentifier->appendTextNode(
595 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
600 $subRecordInfo->appendChild($subRecordIdentifier);
602 $relatedItem->appendChild( $subRecordInfo );
604 my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
605 $tor->setNamespace( "http://www.loc.gov/mods/", "mods", 1 ) if ($tor);
606 $tor = $mods->ownerDocument->importNode($tor) if ($tor);
607 $relatedItem->appendChild($tor) if ($tor);
609 if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
610 $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
611 $part_isbn = $mods->ownerDocument->importNode($part_isbn);
612 $relatedItem->appendChild( $part_isbn );
615 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
619 $mods->appendChild( $relatedItem );
623 $_storage->disconnect;
625 return entityize($mods->toString);
628 __PACKAGE__->register_method(
629 method => 'retrieve_metarecord_mods',
630 api_name => 'open-ils.supercat.metarecord.mods.retrieve',
635 Returns the MODS representation of the requested metarecord
639 { name => 'metarecordId',
640 desc => 'An OpenILS metabib::metarecord id',
644 { desc => 'The metarecord in MODS',
649 sub list_metarecord_formats {
652 { namespace_uri => 'http://www.loc.gov/mods/',
653 docs => 'http://www.loc.gov/mods/',
654 schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
659 for my $type ( keys %metarecord_xslt ) {
662 { namespace_uri => $metarecord_xslt{$type}{namespace_uri},
663 docs => $metarecord_xslt{$type}{docs},
664 schema_location => $metarecord_xslt{$type}{schema_location},
671 __PACKAGE__->register_method(
672 method => 'list_metarecord_formats',
673 api_name => 'open-ils.supercat.metarecord.formats',
678 Returns the list of valid metarecord formats that supercat understands.
681 { desc => 'The format list',
687 sub list_record_formats {
690 { namespace_uri => 'http://www.loc.gov/MARC21/slim',
691 docs => 'http://www.loc.gov/marcxml/',
692 schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
697 for my $type ( keys %record_xslt ) {
700 { namespace_uri => $record_xslt{$type}{namespace_uri},
701 docs => $record_xslt{$type}{docs},
702 schema_location => $record_xslt{$type}{schema_location},
709 __PACKAGE__->register_method(
710 method => 'list_record_formats',
711 api_name => 'open-ils.supercat.record.formats',
716 Returns the list of valid record formats that supercat understands.
719 { desc => 'The format list',
730 throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
731 unless (length($isbn) >= 10);
733 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
735 # Create a storage session, since we'll be making muliple requests.
738 # Find the record that has that ISBN.
739 my $bibrec = $_storage->request(
740 'open-ils.storage.direct.metabib.full_rec.search_where.atomic',
741 { tag => '020', subfield => 'a', value => { ilike => $isbn.'%'} }
744 # Go away if we don't have one.
745 return {} unless (@$bibrec);
747 # Find the metarecord for that bib record.
748 my $mr = $_storage->request(
749 'open-ils.storage.direct.metabib.metarecord_source_map.search.source.atomic',
753 # Find the other records for that metarecord.
754 my $records = $_storage->request(
755 'open-ils.storage.direct.metabib.metarecord_source_map.search.metarecord.atomic',
759 # Just to be safe. There's currently no unique constraint on sources...
760 my %unique_recs = map { ($_->source, 1) } @$records;
761 my @rec_list = sort keys %unique_recs;
763 # And now fetch the ISBNs for thos records.
764 my $recs = $_storage->request(
765 'open-ils.storage.direct.metabib.full_rec.search_where.atomic',
766 { tag => '020', subfield => 'a', record => \@rec_list }
769 # We're done with the storage server session.
770 $_storage->disconnect;
772 # Return the oISBN data structure. This will be XMLized at a higher layer.
774 { metarecord => $mr->[0]->metarecord,
775 record_list => { map { ($_->record, $_->value) } @$recs } };
778 __PACKAGE__->register_method(
780 api_name => 'open-ils.supercat.oisbn',
785 Returns the ISBN list for the metarecord of the requested isbn
790 desc => 'An ISBN. Duh.',
794 { desc => 'record to isbn map',