1 # OpenILS::WWW::OAI manages OAI2 requests and responses.
3 # Copyright (c) 2014-2017 International Institute of Social History
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # Author: Lucien van Wouw <lwo@iisg.nl>
22 package OpenILS::Application::OAI;
23 use strict; use warnings;
25 use base qw/OpenILS::Application/;
26 use OpenSRF::AppSession;
27 use OpenSRF::EX qw(:try);
29 use MARC::File::XML ( BinaryEncoding => 'UTF-8' );
30 use OpenSRF::Utils::SettingsClient;
31 use OpenSRF::Utils::Logger qw($logger);
41 %authority_browse_axis_cache,
51 $_parser = new XML::LibXML;
54 $_xslt = new XML::LibXSLT;
56 # Load the metadataformats that are configured.
57 my $metadata_format = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'metadataformat'};
58 if ( $metadata_format ) {
59 for my $schema ( keys %$metadata_format ) {
60 $logger->info('Loading schema ' . $schema) ;
61 $record_xslt{$schema}{namespace_uri} = $metadata_format->{$schema}->{namespace_uri};
62 $record_xslt{$schema}{schema_location} = $metadata_format->{$schema}->{schema_location};
63 $record_xslt{$schema}{xslt} = $_xslt->parse_stylesheet( $_parser->parse_file(
64 OpenSRF::Utils::SettingsClient->new->config_value( dirs => 'xsl' ) . '/' . $metadata_format->{$schema}->{xslt}
69 # Fall back on system defaults if oai_dc is not set in the configuration.
70 unless ( exists $record_xslt{oai_dc} ) {
71 $logger->info('Loading default oai_dc schema') ;
72 my $xslt = $_parser->parse_file(
73 OpenSRF::Utils::SettingsClient
75 ->config_value( dirs => 'xsl' ).
78 # and stash a transformer
79 $record_xslt{oai_dc}{xslt} = $_xslt->parse_stylesheet( $xslt );
80 $record_xslt{oai_dc}{namespace_uri} = 'http://www.openarchives.org/OAI/2.0/oai_dc/';
81 $record_xslt{oai_dc}{schema_location} = 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd';
84 # If not defined, use the natural marcxml metadata setting
85 unless ( exists $record_xslt{marcxml}) {
86 $logger->info('Loading default marcxml schema') ;
87 my $xslt = $_parser->parse_file(
88 OpenSRF::Utils::SettingsClient
90 ->config_value( dirs => 'xsl' ).
91 "/OAI2_MARC21slim.xsl"
93 # and stash a transformer
94 $record_xslt{marcxml}{xslt} = $_xslt->parse_stylesheet( $xslt );
95 $record_xslt{marcxml}{namespace_uri} = 'http://www.loc.gov/MARC21/slim';
96 $record_xslt{marcxml}{docs} = 'http://www.loc.gov/MARC21/slim';
97 $record_xslt{marcxml}{schema_location} = 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd';
100 # Load the mapping of 852 holdings.
101 my $copies = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'copies'} ;
103 foreach my $subfield_code (keys %$copies) {
104 my $value = $copies->{$subfield_code};
105 $logger->info('Set 852 map ' . $subfield_code . '=' . $value );
106 $copies{$subfield_code} = $value;
108 } else { # if not defined, fall back on these defaults.
119 # Set the barcode filter and status filter
120 $barcode_filter = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'barcode_filter'};
121 $status_filter = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'status_filter'};
127 sub list_record_formats {
130 for my $type ( keys %record_xslt ) {
133 { namespace_uri => $record_xslt{$type}{namespace_uri},
134 docs => $record_xslt{$type}{docs},
135 schema_location => $record_xslt{$type}{schema_location},
143 __PACKAGE__->register_method(
144 method => 'list_record_formats',
145 api_name => 'open-ils.oai.record.formats',
150 desc => 'Returns the list of valid record formats that oai understands.',
153 desc => 'The format list.',
160 sub oai_biblio_retrieve {
165 my $metadataPrefix = shift;
167 # holdings hold an array of call numbers, which hold an array of copies
168 # holdings => [ label: { library, [ copies: { barcode, location, status, circ_lib } ] } ]
171 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
173 # Retrieve the bibliographic record and it's copies
174 my $tree = $_storage->request(
175 "open-ils.cstore.direct.biblio.record_entry.retrieve",
179 bre => [qw/marc edit_date call_numbers/],
180 acn => [qw/edit_date copies owning_lib prefix suffix/],
181 acp => [qw/edit_date location status circ_lib parts/],
186 # Create a MARC::Record object with the marc.
187 my $marc = MARC::Record->new_from_xml( $tree->marc, 'UTF8', 'XML');
189 # Retrieve the MFHD where we can find them.
191 if ( substr($marc->leader, 7, 1) eq 's' ) { # serial
192 my $_search = OpenSRF::AppSession->create( 'open-ils.search' );
193 my $_serials = $_search->request('open-ils.search.serial.record.bib.retrieve', $rec_id, 1, 0)->gather(1);
195 for my $sre (@$_serials) {
196 if ( $sre->location ) {
198 my @svr = split( ' -- ', $sre->location );
199 my $cn_label = $svr[-1];
200 $serials{$order}{'label'} = $cn_label ;
201 my $display = @{$sre->basic_holdings_add} ? $sre->basic_holdings_add : $sre->basic_holdings;
202 $serials{$order}{'ser'} = join(', ', @{$display});
207 my $edit_date = $tree->edit_date ;
209 # Prepare a hash of all holdings and serials
210 for my $cn (@{$tree->call_numbers}) {
212 next unless ( $cn->deleted eq 'f' || !$cn->deleted );
214 for my $c (@{$cn->copies}) {
215 $_visible = _cp_is_visible($cn, $c);
216 last if ( $_visible );
218 next unless $_visible;
220 my $cn_label = $cn->label;
221 $holdings{$cn_label}{'owning_lib'} = $cn->owning_lib->shortname;
223 $edit_date = most_recent_date( $cn->edit_date, $edit_date );
225 for my $cp (@{$cn->copies}) {
227 next unless _cp_is_visible($cn, $cp);
228 $edit_date = most_recent_date( $cp->edit_date, $edit_date );
230 # find the corresponding serial.
231 # There is no way of knowing here if the barcode 852$p is a correct match.
234 foreach my $key (sort keys %serials) {
235 my $serial = $serials{$key};
236 if ( $serial->{'label'} eq $cn_label ) {
237 $ser = $serial->{'ser'};
239 delete $serials{$key}; # in case we have several serial holdings with the same call number
243 $holdings{$cn_label}{'order'} = $order ;
245 my $circlib = $cp->circ_lib->shortname ;
246 push @{$holdings{$cn->label}{'copies'}}, {
247 owning_lib => $cn->owning_lib->shortname,
248 callnumber => $cn->label,
249 barcode => $cp->barcode,
250 status => $cp->status->name,
251 location => $cp->location->name,
258 ## Append the holdings and MFHD data to the marc record and apply the stylesheet.
261 # Force record leader to 'a' as our data is always UTF8
262 # Avoids marc8_to_utf8 from being invoked with horrible results
263 # on the off-chance the record leader isn't correct
264 my $ldr = $marc->leader;
265 substr($ldr, 9, 1, 'a');
268 # Expects the record ID in the 001
269 $marc->delete_field($_) for ($marc->field('001'));
270 if (!$marc->field('001')) {
271 $marc->insert_fields_ordered(
272 MARC::Field->new( '001', $rec_id )
276 # Our reference node to prepend nodes to.
277 my $reference = $marc->field('901');
279 $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s
280 foreach my $cn (sort { $holdings{$a}->{'order'} <=> $holdings{$b}->{'order'}} keys %holdings) {
281 foreach my $cp (@{$holdings{$cn}->{'copies'}}) {
282 my $marc_852 = MARC::Field->new(
283 '852', '4', ' ', 0 => 'dummy'); # The dummy is necessary to prevent a validation error.
284 foreach my $subfield_code (sort keys %copies) {
285 my $_cp = $copies{$subfield_code} ;
286 $marc_852->add_subfields($subfield_code, $cp->{$_cp} || $_cp) if ($_cp);
288 $marc_852->delete_subfield(code => '0');
289 $marc->insert_fields_before($reference, $marc_852);
290 if ( $cp->{'ser'} ) {
291 my $marc_866_a = MARC::Field->new( '866', '4', ' ', 'a' => $cp->{'ser'});
292 $marc->insert_fields_after( $marc_852, $marc_866_a ) ;
299 my $xslt = $record_xslt{$metadataPrefix}{xslt} ;
300 my $xml = $xslt->transform( $_parser->parse_string( $marc->as_xml_record()) );
301 return $xslt->output_as_chars( $xml ) ;
305 __PACKAGE__->register_method(
306 method => 'oai_biblio_retrieve',
307 api_name => 'open-ils.oai.biblio.retrieve',
312 desc => 'Returns the MARCXML representation of the requested bibliographic record.',
317 desc => 'An OpenILS biblio::record_entry id.',
321 name => 'metadataPrefix',
322 desc => 'The metadataPrefix of the schema.',
328 desc => 'An string of the XML in the desired schema.',
335 sub most_recent_date {
337 my $date1 = substr(shift, 0, 19) ; # e.g. '2001-02-03T04:05:06+0000' becomes '2001-02-03T04:05:06'
338 my $date2 = substr(shift, 0, 19) ;
339 my $_date1 = $date1 ;
340 my $_date2 = $date2 ;
342 $date1 =~ s/[-T:\.\+]//g ; # '2001-02-03T04:05:06' becomes '20010203040506'
343 $date2 =~ s/[-T:\.\+]//g ;
345 return $_date1 if ( $date1 > $date2) ;
356 if ( ($cp->deleted eq 'f' || !$cp->deleted) &&
357 ( ! $barcode_filter || $cp->barcode =~ /$barcode_filter/ ) &&
358 $cp->location->opac_visible eq 't' &&
359 $cp->status->opac_visible eq 't' &&
360 $cp->opac_visible eq 't' &&
361 $cp->circ_lib->opac_visible eq 't' &&
362 $cn->owning_lib->opac_visible eq 't' &&
363 (! $status_filter || $cp->status->name =~ /$status_filter/ )
372 sub oai_authority_retrieve {
377 my $metadataPrefix = shift;
379 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
381 # Retrieve the authority record
382 my $record = $_storage->request('open-ils.cstore.direct.authority.record_entry.retrieve', $rec_id)->gather(1);
383 my $o = Fieldmapper::authority::record_entry->new($record) ;
384 my $marc = MARC::Record->new_from_xml( $o->marc, 'UTF8', 'XML');
386 # Expects the record ID in the 001
387 $marc->delete_field($_) for ($marc->field('001'));
388 if (!$marc->field('001')) {
389 $marc->insert_fields_ordered(
390 MARC::Field->new( '001', $rec_id )
394 my $xslt = $record_xslt{$metadataPrefix}{xslt} ;
395 my $xml = $record_xslt{$metadataPrefix}{xslt}->transform(
396 $_parser->parse_string( $marc->as_xml_record())
398 return $record_xslt{$metadataPrefix}{xslt}->output_as_chars( $xml ) ;
402 __PACKAGE__->register_method(
403 method => 'oai_authority_retrieve',
404 api_name => 'open-ils.oai.authority.retrieve',
409 desc => 'Returns the MARCXML representation of the requested authority record.',
414 desc => 'An OpenILS authority::record_entry id.',
418 name => 'metadataPrefix',
419 desc => 'The metadataPrefix of the schema.',
425 desc => 'An string of the XML in the desired schema.',
432 sub oai_list_retrieve {
436 my $record_class = shift || 'biblio';
437 my $rec_id = shift || 0;
441 my $max_count = shift;
442 my $deleted_record = shift || 'yes';
445 $query->{'rec_id'} = ($max_count eq 1) ? $rec_id : {'>=' => $rec_id} ;
446 $query->{'set_spec'} = $set if ( $set ); # unsupported
447 $query->{'deleted'} = 'f' unless ( $deleted_record eq 'yes' );
448 $query->{'datestamp'} = {'>=', $from} if ( $from && !$until ) ;
449 $query->{'datestamp'} = {'<=', $until} if ( !$from && $until ) ;
450 $query->{'-and'} = [{'datestamp'=>{'>=' => $from}}, {'datestamp'=>{'<=' => $until}}] if ( $from && $until ) ;
452 my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
453 return $_storage->request('open-ils.cstore.direct.oai.' . $record_class . '.search.atomic',
456 limit => $max_count + 1
461 __PACKAGE__->register_method(
462 method => 'oai_list_retrieve',
463 api_name => 'open-ils.oai.list.retrieve',
468 desc => 'Returns a list of record identifiers.',
472 name => 'record_class',
473 desc => '\'biblio\' for bibliographic records or \'authority\' for authority records',
477 desc => 'An optional rec_id number used as a cursor.',
482 desc => 'The datestamp the resultset range should begin with.',
487 desc => 'The datestamp the resultset range should end with.',
492 desc => 'A setspec.',
497 desc => 'The number of identifiers to return.',
501 name => 'deleted_record',
502 desc => 'If set to \'no\' the response will only include active records.',
508 desc => 'An OAI type record.',