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;
42 # we need an XML parser
43 $_parser = new XML::LibXML;
46 $_xslt = new XML::LibXSLT;
48 # parse the MODS xslt ...
49 my $mods3_xslt = $_parser->parse_file(
50 OpenSRF::Utils::SettingsClient
52 ->config_value( dirs => 'xsl' ).
53 "/MARC21slim2MODS3.xsl"
55 # and stash a transformer
56 $record_xslt{mods3}{xslt} = $_xslt->parse_stylesheet( $mods3_xslt );
57 $record_xslt{mods3}{namespace_uri} = 'http://www.loc.gov/mods/v3';
58 $record_xslt{mods3}{docs} = 'http://www.loc.gov/mods/';
59 $record_xslt{mods3}{schema_location} = 'http://www.loc.gov/standards/mods/v3/mods-3-1.xsd';
61 # parse the MODS xslt ...
62 my $mods_xslt = $_parser->parse_file(
63 OpenSRF::Utils::SettingsClient
65 ->config_value( dirs => 'xsl' ).
66 "/MARC21slim2MODS.xsl"
68 # and stash a transformer
69 $record_xslt{mods}{xslt} = $_xslt->parse_stylesheet( $mods_xslt );
70 $record_xslt{mods}{namespace_uri} = 'http://www.loc.gov/mods/';
71 $record_xslt{mods}{docs} = 'http://www.loc.gov/mods/';
72 $record_xslt{mods}{schema_location} = 'http://www.loc.gov/standards/mods/mods.xsd';
74 # parse the ATOM entry xslt ...
75 my $atom_xslt = $_parser->parse_file(
76 OpenSRF::Utils::SettingsClient
78 ->config_value( dirs => 'xsl' ).
79 "/MARC21slim2ATOM.xsl"
81 # and stash a transformer
82 $record_xslt{atom}{xslt} = $_xslt->parse_stylesheet( $atom_xslt );
83 $record_xslt{atom}{namespace_uri} = 'http://www.w3.org/2005/Atom';
84 $record_xslt{atom}{docs} = 'http://www.ietf.org/rfc/rfc4287.txt';
86 # parse the RDFDC xslt ...
87 my $rdf_dc_xslt = $_parser->parse_file(
88 OpenSRF::Utils::SettingsClient
90 ->config_value( dirs => 'xsl' ).
91 "/MARC21slim2RDFDC.xsl"
93 # and stash a transformer
94 $record_xslt{rdf_dc}{xslt} = $_xslt->parse_stylesheet( $rdf_dc_xslt );
95 $record_xslt{rdf_dc}{namespace_uri} = 'http://purl.org/dc/elements/1.1/';
96 $record_xslt{rdf_dc}{schema_location} = 'http://purl.org/dc/elements/1.1/';
98 # parse the SRWDC xslt ...
99 my $srw_dc_xslt = $_parser->parse_file(
100 OpenSRF::Utils::SettingsClient
102 ->config_value( dirs => 'xsl' ).
103 "/MARC21slim2SRWDC.xsl"
105 # and stash a transformer
106 $record_xslt{srw_dc}{xslt} = $_xslt->parse_stylesheet( $srw_dc_xslt );
107 $record_xslt{srw_dc}{namespace_uri} = 'info:srw/schema/1/dc-schema';
108 $record_xslt{srw_dc}{schema_location} = 'http://www.loc.gov/z3950/agency/zing/srw/dc-schema.xsd';
110 # parse the OAIDC xslt ...
111 my $oai_dc_xslt = $_parser->parse_file(
112 OpenSRF::Utils::SettingsClient
114 ->config_value( dirs => 'xsl' ).
115 "/MARC21slim2OAIDC.xsl"
117 # and stash a transformer
118 $record_xslt{oai_dc}{xslt} = $_xslt->parse_stylesheet( $oai_dc_xslt );
119 $record_xslt{oai_dc}{namespace_uri} = 'http://www.openarchives.org/OAI/2.0/oai_dc/';
120 $record_xslt{oai_dc}{schema_location} = 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd';
122 # parse the RSS xslt ...
123 my $rss_xslt = $_parser->parse_file(
124 OpenSRF::Utils::SettingsClient
126 ->config_value( dirs => 'xsl' ).
127 "/MARC21slim2RSS2.xsl"
129 # and stash a transformer
130 $record_xslt{rss2}{xslt} = $_xslt->parse_stylesheet( $rss_xslt );
132 # and finally, a storage server session
134 register_record_transforms();
139 sub register_record_transforms {
140 for my $type ( keys %record_xslt ) {
141 __PACKAGE__->register_method(
142 method => 'retrieve_record_transform',
143 api_name => "open-ils.supercat.record.$type.retrieve",
147 { desc => "Returns the \U$type\E representation ".
148 "of the requested bibliographic record",
152 desc => 'An OpenILS biblio::record_entry id',
156 { desc => "The bib record in \U$type\E",
165 my $stuff = NFC(shift());
166 $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
178 my ($d,$m,$y) = (localtime)[4,5,6];
179 $when = sprintf('%4d-%02d-%02d', $y + 1900, $m + 1, $d);
183 $type = 'authority' if ($self->api_name =~ /authority/o);
185 my $axis = 'create_date';
186 $axis = 'edit_date' if ($self->api_name =~ /edit/o);
188 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
192 "open-ils.storage.id_list.$type.record_entry.search_where.atomic",
193 { $axis => { ">" => $when } },
194 { order_by => "$axis desc", limit => $limit } )
198 for my $t ( qw/biblio authority/ ) {
199 for my $a ( qw/import edit/ ) {
201 __PACKAGE__->register_method(
202 method => 'recent_changes',
203 api_name => "open-ils.supercat.$t.record.$a.recent",
207 { desc => "Returns a list of recently ${a}ed $t records",
211 desc => "Date to start looking for ${a}ed records",
216 desc => "Maximum count to retrieve",
220 { desc => "An id list of $t records",
228 sub retrieve_record_marcxml {
233 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
238 ->request( 'open-ils.storage.direct.biblio.record_entry.retrieve' => $rid )
244 __PACKAGE__->register_method(
245 method => 'retrieve_record_marcxml',
246 api_name => 'open-ils.supercat.record.marcxml.retrieve',
251 Returns the MARCXML representation of the requested bibliographic record
256 desc => 'An OpenILS biblio::record_entry id',
260 { desc => 'The bib record in MARCXML',
265 sub retrieve_record_transform {
270 (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
272 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
274 warn "Fetching record entry $rid\n";
275 my $marc = $_storage->request(
276 'open-ils.storage.direct.biblio.record_entry.retrieve',
279 warn "Fetched record entry $rid\n";
281 return entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $marc ) )->toString);
285 sub retrieve_metarecord_mods {
290 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
295 # Get the metarecord in question
298 'open-ils.storage.direct.metabib.metarecord.retrieve' => $rid
301 # Now get the map of all bib records for the metarecord
304 'open-ils.storage.direct.metabib.metarecord_source_map.search.metarecord.atomic',
308 $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
310 # and retrieve the lead (master) record as MODS
312 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
313 ->run($mr->master_record);
314 my $master_mods = $_parser->parse_string($master)->documentElement;
315 $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
317 # ... and a MODS clone to populate, with guts removed.
318 my $mods = $_parser->parse_string($master)->documentElement;
319 $mods->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
320 ($mods) = $mods->findnodes('//mods:mods');
321 $mods->removeChildNodes;
323 # Add the metarecord ID as a (locally defined) info URI
324 my $recordInfo = $mods
326 ->createElement("mods:recordInfo");
328 my $recordIdentifier = $mods
330 ->createElement("mods:recordIdentifier");
332 my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
337 $recordIdentifier->appendTextNode(
338 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
341 $recordInfo->appendChild($recordIdentifier);
342 $mods->appendChild($recordInfo);
344 # Grab the title, author and ISBN for the master record and populate the metarecord
345 my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
348 $title->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
349 $title = $mods->ownerDocument->importNode($title);
350 $mods->appendChild($title);
353 my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
355 $author->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
356 $author = $mods->ownerDocument->importNode($author);
357 $mods->appendChild($author);
360 my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
362 $isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
363 $isbn = $mods->ownerDocument->importNode($isbn);
364 $mods->appendChild($isbn);
367 # ... and loop over the constituent records
368 for my $map ( @$recs ) {
372 $self ->method_lookup('open-ils.supercat.record.mods.retrieve')
375 my $part_mods = $_parser->parse_string($rec);
376 $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
377 ($part_mods) = $part_mods->findnodes('//mods:mods');
379 for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
380 $node->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
381 $node = $mods->ownerDocument->importNode($node);
382 $mods->appendChild( $node );
385 my $relatedItem = $mods
387 ->createElement("mods:relatedItem");
389 $relatedItem->setAttribute( type => 'constituent' );
391 my $identifier = $mods
393 ->createElement("mods:identifier");
395 $identifier->setAttribute( type => 'uri' );
397 my $subRecordInfo = $mods
399 ->createElement("mods:recordInfo");
401 my $subRecordIdentifier = $mods
403 ->createElement("mods:recordIdentifier");
405 my $subid = $map->source;
406 $subRecordIdentifier->appendTextNode(
407 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
412 $subRecordInfo->appendChild($subRecordIdentifier);
414 $relatedItem->appendChild( $subRecordInfo );
416 my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
417 $tor->setNamespace( "http://www.loc.gov/mods/", "mods", 1 ) if ($tor);
418 $tor = $mods->ownerDocument->importNode($tor) if ($tor);
419 $relatedItem->appendChild($tor) if ($tor);
421 if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
422 $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods", 1 );
423 $part_isbn = $mods->ownerDocument->importNode($part_isbn);
424 $relatedItem->appendChild( $part_isbn );
427 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
431 $mods->appendChild( $relatedItem );
435 $_storage->disconnect;
437 return entityize($mods->toString);
440 __PACKAGE__->register_method(
441 method => 'retrieve_metarecord_mods',
442 api_name => 'open-ils.supercat.metarecord.mods.retrieve',
447 Returns the MODS representation of the requested metarecord
451 { name => 'metarecordId',
452 desc => 'An OpenILS metabib::metarecord id',
456 { desc => 'The metarecord in MODS',
461 sub list_metarecord_formats {
464 { namespace_uri => 'http://www.loc.gov/mods/',
465 docs => 'http://www.loc.gov/mods/',
466 schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
471 for my $type ( keys %metarecord_xslt ) {
474 { namespace_uri => $metarecord_xslt{$type}{namespace_uri},
475 docs => $metarecord_xslt{$type}{docs},
476 schema_location => $metarecord_xslt{$type}{schema_location},
483 __PACKAGE__->register_method(
484 method => 'list_metarecord_formats',
485 api_name => 'open-ils.supercat.metarecord.formats',
490 Returns the list of valid metarecord formats that supercat understands.
493 { desc => 'The format list',
499 sub list_record_formats {
502 { namespace_uri => 'http://www.loc.gov/MARC21/slim',
503 docs => 'http://www.loc.gov/marcxml/',
504 schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
509 for my $type ( keys %record_xslt ) {
512 { namespace_uri => $record_xslt{$type}{namespace_uri},
513 docs => $record_xslt{$type}{docs},
514 schema_location => $record_xslt{$type}{schema_location},
521 __PACKAGE__->register_method(
522 method => 'list_record_formats',
523 api_name => 'open-ils.supercat.record.formats',
528 Returns the list of valid record formats that supercat understands.
531 { desc => 'The format list',
542 throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
543 unless (length($isbn) >= 10);
545 my $_storage = OpenSRF::AppSession->create( 'open-ils.storage' );
547 # Create a storage session, since we'll be making muliple requests.
550 # Find the record that has that ISBN.
551 my $bibrec = $_storage->request(
552 'open-ils.storage.direct.metabib.full_rec.search_where.atomic',
553 { tag => '020', subfield => 'a', value => { ilike => $isbn.'%'} }
556 # Go away if we don't have one.
557 return {} unless (@$bibrec);
559 # Find the metarecord for that bib record.
560 my $mr = $_storage->request(
561 'open-ils.storage.direct.metabib.metarecord_source_map.search.source.atomic',
565 # Find the other records for that metarecord.
566 my $records = $_storage->request(
567 'open-ils.storage.direct.metabib.metarecord_source_map.search.metarecord.atomic',
571 # Just to be safe. There's currently no unique constraint on sources...
572 my %unique_recs = map { ($_->source, 1) } @$records;
573 my @rec_list = sort keys %unique_recs;
575 # And now fetch the ISBNs for thos records.
576 my $recs = $_storage->request(
577 'open-ils.storage.direct.metabib.full_rec.search_where.atomic',
578 { tag => '020', subfield => 'a', record => \@rec_list }
581 # We're done with the storage server session.
582 $_storage->disconnect;
584 # Return the oISBN data structure. This will be XMLized at a higher layer.
586 { metarecord => $mr->[0]->metarecord,
587 record_list => { map { ($_->record, $_->value) } @$recs } };
590 __PACKAGE__->register_method(
592 api_name => 'open-ils.supercat.oisbn',
597 Returns the ISBN list for the metarecord of the requested isbn
602 desc => 'An ISBN. Duh.',
606 { desc => 'record to isbn map',