]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm
LP#1729620 Cleanup, fix bugs
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / OAI.pm
1 # OpenILS::WWW::OAI manages OAI2 requests and responses.
2 #
3 # Copyright (c) 2014-2017 International Institute of Social History
4 #
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.
9 #
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.
14 #
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/>.
17 #
18 #
19 # Author: Lucien van Wouw <lwo@iisg.nl>
20
21
22 package OpenILS::Application::OAI;
23 use strict; use warnings;
24
25 use base qw/OpenILS::Application/;
26 use OpenSRF::AppSession;
27 use OpenSRF::EX qw(:try);
28 use MARC::Record;
29 use MARC::File::XML ( BinaryEncoding => 'UTF-8' );
30 use OpenSRF::Utils::SettingsClient;
31 use OpenSRF::Utils::Logger qw($logger);
32 use XML::LibXML;
33 use XML::LibXSLT;
34
35 my (
36   $_parser,
37   $_xslt,
38   %record_xslt,
39   %metarecord_xslt,
40   %holdings_data_cache,
41   %authority_browse_axis_cache,
42   %copies,
43   $barcode_filter,
44   $status_filter
45 );
46
47
48 sub child_init {
49
50     # set the XML parser
51     $_parser = new XML::LibXML;
52
53     # and the xslt parser
54     $_xslt = new XML::LibXSLT;
55
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}
65             ) );
66         }
67     }
68
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
74                 ->new
75                 ->config_value( dirs => 'xsl' ).
76             "/OAI2_OAIDC.xsl"
77         );
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';
82     }
83
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
89                 ->new
90                 ->config_value( dirs => 'xsl' ).
91             "/OAI2_MARC21slim.xsl"
92         );
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';
98     }
99
100     # Load the mapping of 852 holdings.
101     my $copies = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'copies'} ;
102     if ( $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;
107         }
108     } else { # if not defined, fall back on these defaults.
109         %copies = (
110             a => 'location',
111             b => 'owning_lib',
112             c => 'callnumber',
113             d => 'circlib',
114             g => 'barcode',
115             n => 'status'
116         );
117     }
118
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'};
122
123     return 1;
124 }
125
126
127 sub list_record_formats {
128
129     my @list;
130     for my $type ( keys %record_xslt ) {
131         push @list,
132             { $type =>
133                 { namespace_uri   => $record_xslt{$type}{namespace_uri},
134                   docs        => $record_xslt{$type}{docs},
135                   schema_location => $record_xslt{$type}{schema_location},
136                 }
137             };
138     }
139
140     return \@list;
141 }
142
143 __PACKAGE__->register_method(
144     method    => 'list_record_formats',
145     api_name  => 'open-ils.oai.record.formats',
146     api_level => 1,
147     argc      => 0,
148     signature =>
149     {
150         desc     => 'Returns the list of valid record formats that oai understands.',
151         'return' =>
152         {
153             desc => 'The format list.',
154             type => 'array'
155         }
156     }
157 );
158
159
160 sub oai_biblio_retrieve {
161
162     my $self = shift;
163     my $client = shift;
164     my $rec_id = shift;
165     my $metadataPrefix = shift;
166
167     #  holdings hold an array of call numbers, which hold an array of copies
168     #  holdings => [ label: { library, [ copies: { barcode, location, status, circ_lib } ] } ]
169     my %holdings;
170
171     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
172
173     # Retrieve the bibliographic record and it's copies
174     my $tree = $_storage->request(
175         "open-ils.cstore.direct.biblio.record_entry.retrieve",
176         $rec_id,
177         { flesh     => 5,
178           flesh_fields  => {
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/],
182                 }
183         }
184     )->gather(1);
185
186     # Create a MARC::Record object with the marc.
187     my $marc = MARC::Record->new_from_xml( $tree->marc, 'UTF8', 'XML');
188
189     # Retrieve the MFHD where we can find them.
190     my %serials;
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);
194         my $order = 0 ;
195         for my $sre (@$_serials) {
196             if ( $sre->location ) {
197                 $order++ ;
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});
203             }
204         }
205     }
206
207     my $edit_date = $tree->edit_date ;
208
209     # Prepare a hash of all holdings and serials
210     for my $cn (@{$tree->call_numbers}) {
211
212         next unless ( $cn->deleted eq 'f' || !$cn->deleted );
213         my $_visible = 0;
214         for my $c (@{$cn->copies}) {
215             $_visible = _cp_is_visible($cn, $c);
216             last if ( $_visible );
217         }
218         next unless $_visible;
219
220         my $cn_label = $cn->label;
221         $holdings{$cn_label}{'owning_lib'} = $cn->owning_lib->shortname;
222
223         $edit_date =  most_recent_date( $cn->edit_date, $edit_date );
224
225         for my $cp (@{$cn->copies}) {
226
227             next unless _cp_is_visible($cn, $cp);
228             $edit_date = most_recent_date( $cp->edit_date, $edit_date );
229
230             # find the corresponding serial.
231             # There is no way of knowing here if the barcode 852$p is a correct match.
232             my $order = 0 ;
233             my $ser;
234             foreach my $key (sort keys %serials) {
235                 my $serial = $serials{$key};
236                 if ( $serial->{'label'} eq $cn_label ) {
237                     $ser = $serial->{'ser'};
238                     $order = $key;
239                     delete $serials{$key}; # in case we have several serial holdings with the same call number
240                     last;
241                }
242             }
243             $holdings{$cn_label}{'order'} = $order ;
244
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,
252                 circlib    => $circlib,
253                 ser        => $ser
254             };
255         }
256     }
257
258     ## Append the holdings and MFHD data to the marc record and apply the stylesheet.
259     if ( %holdings ) {
260
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');
266         $marc->leader($ldr);
267
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 )
273             );
274         }
275
276         # Our reference node to prepend nodes to.
277         my $reference = $marc->field('901');
278
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);
287                 }
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 ) ;
293                 }
294             }
295         }
296
297     }
298
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 ) ;
302 }
303
304
305 __PACKAGE__->register_method(
306     method    => 'oai_biblio_retrieve',
307     api_name  => 'open-ils.oai.biblio.retrieve',
308     api_level => 1,
309     argc      => 1,
310     signature =>
311     {
312         desc     => 'Returns the MARCXML representation of the requested bibliographic record.',
313         params   =>
314         [
315             {
316                 name => 'rec_id',
317                 desc => 'An OpenILS biblio::record_entry id.',
318                 type => 'number'
319             },
320             {
321                 name => 'metadataPrefix',
322                 desc => 'The metadataPrefix of the schema.',
323                 type => 'string'
324             }
325         ],
326         'return' =>
327         {
328             desc => 'An string of the XML in the desired schema.',
329             type => 'string'
330         }
331     }
332 );
333
334
335 sub most_recent_date {
336
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 ;
341
342     $date1 =~ s/[-T:\.\+]//g ; # '2001-02-03T04:05:06' becomes '20010203040506'
343     $date2 =~ s/[-T:\.\+]//g ;
344
345     return $_date1 if ( $date1 > $date2) ;
346     return $_date2 ;
347 }
348
349
350 sub _cp_is_visible {
351
352     my $cn = shift;
353     my $cp = shift;
354
355     my $visible = 0;
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/ )
364     ) {
365         $visible = 1;
366     }
367
368     return $visible;
369 }
370
371
372 sub oai_authority_retrieve {
373
374     my $self = shift;
375     my $client = shift;
376     my $rec_id = shift;
377     my $metadataPrefix = shift;
378
379     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
380
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');
385
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 )
391         );
392     }
393
394     my $xslt = $record_xslt{$metadataPrefix}{xslt} ;
395     my $xml = $record_xslt{$metadataPrefix}{xslt}->transform(
396        $_parser->parse_string( $marc->as_xml_record())
397     );
398     return $record_xslt{$metadataPrefix}{xslt}->output_as_chars( $xml ) ;
399 }
400
401
402 __PACKAGE__->register_method(
403     method    => 'oai_authority_retrieve',
404     api_name  => 'open-ils.oai.authority.retrieve',
405     api_level => 1,
406     argc      => 1,
407     signature =>
408     {
409         desc     => 'Returns the MARCXML representation of the requested authority record.',
410         params   =>
411         [
412             {
413                 name => 'rec_id',
414                 desc => 'An OpenILS authority::record_entry id.',
415                 type => 'number'
416             },
417             {
418                 name => 'metadataPrefix',
419                 desc => 'The metadataPrefix of the schema.',
420                 type => 'string'
421             }
422         ],
423         'return' =>
424         {
425             desc => 'An string of the XML in the desired schema.',
426             type => 'string'
427         }
428     }
429 );
430
431
432 sub oai_list_retrieve {
433
434     my $self            = shift;
435     my $client          = shift;
436     my $record_class    = shift || 'biblio';
437     my $rec_id          = shift || 0;
438     my $from            = shift;
439     my $until           = shift;
440     my $set             = shift ;
441     my $max_count       = shift;
442     my $deleted_record  = shift || 'yes';
443
444     my $query = {};
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 ) ;
451
452     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
453     return $_storage->request('open-ils.cstore.direct.oai.' . $record_class . '.search.atomic',
454             $query,
455             {
456                 limit => $max_count + 1
457             }
458         )->gather(1);
459 }
460
461 __PACKAGE__->register_method(
462     method    => 'oai_list_retrieve',
463     api_name  => 'open-ils.oai.list.retrieve',
464     api_level => 1,
465     argc      => 1,
466     signature =>
467     {
468         desc => 'Returns a list of record identifiers.',
469         params =>
470         [
471             {
472                 name => 'record_class',
473                 desc => '\'biblio\' for bibliographic records or \'authority\' for authority records',
474                 type => 'string'
475             },            {
476                 name => 'rec_id',
477                 desc => 'An optional rec_id number used as a cursor.',
478                 type => 'number'
479             },
480             {
481                 name => 'from',
482                 desc => 'The datestamp the resultset range should begin with.',
483                 type => 'string'
484             },
485             {
486                 name => 'until',
487                 desc => 'The datestamp the resultset range should end with.',
488                 type => 'string'
489             },
490             {
491                 name => 'set',
492                 desc => 'A setspec.',
493                 type => 'string'
494             },
495             {
496                 name => 'max_count',
497                 desc => 'The number of identifiers to return.',
498                 type => 'number'
499             },
500             {
501                 name => 'deleted_record',
502                 desc => 'If set to \'no\' the response will only include active records.',
503                 type => 'string'
504             }
505         ],
506         'return' =>
507         {
508             desc => 'An OAI type record.',
509             type => 'array'
510         }
511     }
512 );
513
514
515 1;