Bug 11622 [QA Followup]
[koha.git] / opac / oai.pl
1 #!/usr/bin/perl
2
3 # Copyright Biblibre 2008
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
21 use strict;
22 use warnings;
23
24 use CGI qw( :standard -oldstyle_urls -utf8 );
25 use vars qw( $GZIP );
26 use C4::Context;
27
28
29 BEGIN {
30     eval { require PerlIO::gzip };
31     $GZIP = ($@) ? 0 : 1;
32 }
33
34 unless ( C4::Context->preference('OAI-PMH') ) {
35     print
36         header(
37             -type       => 'text/plain; charset=utf-8',
38             -charset    => 'utf-8',
39             -status     => '404 OAI-PMH service is disabled',
40         ),
41         "OAI-PMH service is disabled";
42     exit;
43 }
44
45 my @encodings = http('HTTP_ACCEPT_ENCODING');
46 if ( $GZIP && grep { defined($_) && $_ eq 'gzip' } @encodings ) {
47     print header(
48         -type               => 'text/xml; charset=utf-8',
49         -charset            => 'utf-8',
50         -Content-Encoding   => 'gzip',
51     );
52     binmode( STDOUT, ":gzip" );
53 }
54 else {
55     print header(
56         -type       => 'text/xml; charset=utf-8',
57         -charset    => 'utf-8',
58     );
59 }
60
61 binmode STDOUT, ':encoding(UTF-8)';
62 my $repository = C4::OAI::Repository->new();
63
64
65
66
67 # __END__ Main Prog
68
69
70 #
71 # Extends HTTP::OAI::ResumptionToken
72 # A token is identified by:
73 # - metadataPrefix
74 # - from
75 # - until
76 # - offset
77 #
78 package C4::OAI::ResumptionToken;
79
80 use strict;
81 use warnings;
82 use HTTP::OAI;
83
84 use base ("HTTP::OAI::ResumptionToken");
85
86
87 sub new {
88     my ($class, %args) = @_;
89
90     my $self = $class->SUPER::new(%args);
91
92     my ($metadata_prefix, $offset, $from, $until, $set);
93     if ( $args{ resumptionToken } ) {
94         ($metadata_prefix, $offset, $from, $until, $set)
95             = split( '/', $args{resumptionToken} );
96     }
97     else {
98         $metadata_prefix = $args{ metadataPrefix };
99         $from = $args{ from } || '1970-01-01';
100         $until = $args{ until };
101         unless ( $until) {
102             my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime( time );
103             $until = sprintf( "%.4d-%.2d-%.2d", $year+1900, $mon+1,$mday );
104         }
105         #Add times to the arguments, when necessary, so they correctly match against the DB timestamps
106         $from .= 'T00:00:00Z' if length($from) == 10;
107         $until .= 'T23:59:59Z' if length($until) == 10;
108         $offset = $args{ offset } || 0;
109         $set = $args{set} || '';
110     }
111
112     $self->{ metadata_prefix } = $metadata_prefix;
113     $self->{ offset          } = $offset;
114     $self->{ from            } = $from;
115     $self->{ until           } = $until;
116     $self->{ set             } = $set;
117     $self->{ from_arg        } = _strip_UTC_designators($from);
118     $self->{ until_arg       } = _strip_UTC_designators($until);
119
120     $self->resumptionToken(
121         join( '/', $metadata_prefix, $offset, $from, $until, $set ) );
122     $self->cursor( $offset );
123
124     return $self;
125 }
126
127 sub _strip_UTC_designators {
128     my ( $timestamp ) = @_;
129     $timestamp =~ s/T/ /g;
130     $timestamp =~ s/Z//g;
131     return $timestamp;
132 }
133
134 # __END__ C4::OAI::ResumptionToken
135
136
137
138 package C4::OAI::Identify;
139
140 use strict;
141 use warnings;
142 use HTTP::OAI;
143 use C4::Context;
144
145 use base ("HTTP::OAI::Identify");
146
147 sub new {
148     my ($class, $repository) = @_;
149
150     my ($baseURL) = $repository->self_url() =~ /(.*)\?.*/;
151     my $self = $class->SUPER::new(
152         baseURL             => $baseURL,
153         repositoryName      => C4::Context->preference("LibraryName"),
154         adminEmail          => C4::Context->preference("KohaAdminEmailAddress"),
155         MaxCount            => C4::Context->preference("OAI-PMH:MaxCount"),
156         granularity         => 'YYYY-MM-DD',
157         earliestDatestamp   => '0001-01-01',
158         deletedRecord       => C4::Context->preference("OAI-PMH:DeletedRecord") || 'no',
159     );
160
161     # FIXME - alas, the description element is not so simple; to validate
162     # against the OAI-PMH schema, it cannot contain just a string,
163     # but one or more elements that validate against another XML schema.
164     # For now, simply omitting it.
165     # $self->description( "Koha OAI Repository" );
166
167     $self->compression( 'gzip' );
168
169     return $self;
170 }
171
172 # __END__ C4::OAI::Identify
173
174
175
176 package C4::OAI::ListMetadataFormats;
177
178 use strict;
179 use warnings;
180 use HTTP::OAI;
181
182 use base ("HTTP::OAI::ListMetadataFormats");
183
184 sub new {
185     my ($class, $repository) = @_;
186
187     my $self = $class->SUPER::new();
188
189     if ( $repository->{ conf } ) {
190         foreach my $name ( @{ $repository->{ koha_metadata_format } } ) {
191             my $format = $repository->{ conf }->{ format }->{ $name };
192             $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
193                 metadataPrefix    => $format->{metadataPrefix},
194                 schema            => $format->{schema},
195                 metadataNamespace => $format->{metadataNamespace}, ) );
196         }
197     }
198     else {
199         $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
200             metadataPrefix    => 'oai_dc',
201             schema            => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
202             metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/'
203         ) );
204         $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
205             metadataPrefix    => 'marcxml',
206             schema            => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd',
207             metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim'
208         ) );
209     }
210
211     return $self;
212 }
213
214 # __END__ C4::OAI::ListMetadataFormats
215
216
217
218 package C4::OAI::Record;
219
220 use strict;
221 use warnings;
222 use HTTP::OAI;
223 use HTTP::OAI::Metadata::OAI_DC;
224
225 use base ("HTTP::OAI::Record");
226
227 sub new {
228     my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_;
229
230     my $self = $class->SUPER::new(%args);
231
232     $timestamp =~ s/ /T/, $timestamp .= 'Z';
233     $self->header( new HTTP::OAI::Header(
234         identifier  => $args{identifier},
235         datestamp   => $timestamp,
236     ) );
237
238     foreach my $setSpec (@$setSpecs) {
239         $self->header->setSpec($setSpec);
240     }
241
242     my $parser = XML::LibXML->new();
243     my $record_dom = $parser->parse_string( $marcxml );
244     my $format =  $args{metadataPrefix};
245     if ( $format ne 'marcxml' ) {
246         my %args = (
247             OPACBaseURL => "'" . C4::Context->preference('OPACBaseURL') . "'"
248         );
249         $record_dom = $repository->stylesheet($format)->transform($record_dom, %args);
250     }
251     $self->metadata( HTTP::OAI::Metadata->new( dom => $record_dom ) );
252
253     return $self;
254 }
255
256 # __END__ C4::OAI::Record
257
258 package C4::OAI::DeletedRecord;
259
260 use Modern::Perl;
261 use HTTP::OAI;
262 use HTTP::OAI::Metadata::OAI_DC;
263
264 use base ("HTTP::OAI::Record");
265
266 sub new {
267     my ($class, $timestamp, $setSpecs, %args) = @_;
268
269     my $self = $class->SUPER::new(%args);
270
271     $timestamp =~ s/ /T/, $timestamp .= 'Z';
272     $self->header( new HTTP::OAI::Header(
273         status      => 'deleted',
274         identifier  => $args{identifier},
275         datestamp   => $timestamp,
276     ) );
277
278     foreach my $setSpec (@$setSpecs) {
279         $self->header->setSpec($setSpec);
280     }
281
282     return $self;
283 }
284
285 # __END__ C4::OAI::DeletedRecord
286
287
288
289 package C4::OAI::GetRecord;
290
291 use strict;
292 use warnings;
293 use HTTP::OAI;
294 use C4::Biblio;
295 use C4::OAI::Sets;
296 use MARC::File::XML;
297
298 use base ("HTTP::OAI::GetRecord");
299
300
301 sub new {
302     my ($class, $repository, %args) = @_;
303
304     my $self = HTTP::OAI::GetRecord->new(%args);
305
306     my $dbh = C4::Context->dbh;
307     my $sth = $dbh->prepare("
308         SELECT timestamp
309         FROM   biblioitems
310         WHERE  biblionumber=? " );
311     my $prefix = $repository->{koha_identifier} . ':';
312     my ($biblionumber) = $args{identifier} =~ /^$prefix(.*)/;
313     $sth->execute( $biblionumber );
314     my ($timestamp, $deleted);
315     unless ( ($timestamp) = $sth->fetchrow ) {
316         unless ( ($timestamp) = $dbh->selectrow_array(q/
317             SELECT timestamp
318             FROM deletedbiblio
319             WHERE biblionumber=? /, undef, $biblionumber ))
320         {
321             return HTTP::OAI::Response->new(
322              requestURL  => $repository->self_url(),
323              errors      => [ new HTTP::OAI::Error(
324                 code    => 'idDoesNotExist',
325                 message => "There is no biblio record with this identifier",
326                 ) ],
327             );
328         }
329         else {
330             $deleted = 1;
331         }
332     }
333
334     # We fetch it using this method, rather than the database directly,
335     # so it'll include the item data
336     my $marcxml;
337     $marcxml = $repository->get_biblio_marcxml($biblionumber, $args{metadataPrefix})
338         unless $deleted;
339     my $oai_sets = GetOAISetsBiblio($biblionumber);
340     my @setSpecs;
341     foreach (@$oai_sets) {
342         push @setSpecs, $_->{spec};
343     }
344
345     #$self->header( HTTP::OAI::Header->new( identifier  => $args{identifier} ) );
346     $self->record(
347         $deleted
348         ? C4::OAI::DeletedRecord->new($timestamp, \@setSpecs, %args)
349         : C4::OAI::Record->new($repository, $marcxml, $timestamp, \@setSpecs, %args)
350     );
351     return $self;
352 }
353
354 # __END__ C4::OAI::GetRecord
355
356
357
358 package C4::OAI::ListIdentifiers;
359
360 use strict;
361 use warnings;
362 use HTTP::OAI;
363 use C4::OAI::Sets;
364
365 use base ("HTTP::OAI::ListIdentifiers");
366
367
368 sub new {
369     my ($class, $repository, %args) = @_;
370
371     my $self = HTTP::OAI::ListIdentifiers->new(%args);
372
373     my $token = new C4::OAI::ResumptionToken( %args );
374     my $dbh = C4::Context->dbh;
375     my $set;
376     if(defined $token->{'set'}) {
377         $set = GetOAISetBySpec($token->{'set'});
378     }
379     my $max = $repository->{koha_max_count};
380     my $sql = "
381         (SELECT biblioitems.biblionumber, biblioitems.timestamp
382         FROM biblioitems
383     ";
384     $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
385     $sql .= " WHERE timestamp >= ? AND timestamp <= ? ";
386     $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
387     $sql .= ") UNION
388         (SELECT deletedbiblio.biblionumber, timestamp FROM deletedbiblio";
389     $sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
390     $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? ";
391     $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
392
393     $sql .= ") ORDER BY biblionumber
394         LIMIT " . ($max+1) . "
395         OFFSET $token->{offset}
396     ";
397     my $sth = $dbh->prepare( $sql );
398     my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'});
399     push @bind_params, $set->{'id'} if defined $set;
400     push @bind_params, ($token->{'from'}, $token->{'until'});
401     push @bind_params, $set->{'id'} if defined $set;
402     $sth->execute( @bind_params );
403
404     my $count = 0;
405     while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) {
406         $count++;
407         if ( $count > $max ) {
408             $self->resumptionToken(
409                 new C4::OAI::ResumptionToken(
410                     metadataPrefix  => $token->{metadata_prefix},
411                     from            => $token->{from},
412                     until           => $token->{until},
413                     offset          => $token->{offset} + $max,
414                     set             => $token->{set}
415                 )
416             );
417             last;
418         }
419         $timestamp =~ s/ /T/, $timestamp .= 'Z';
420         $self->identifier( new HTTP::OAI::Header(
421             identifier => $repository->{ koha_identifier} . ':' . $biblionumber,
422             datestamp  => $timestamp,
423         ) );
424     }
425
426     # Return error if no results
427     unless ($count) {
428         return HTTP::OAI::Response->new(
429             requestURL => $repository->self_url(),
430             errors     => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ],
431         );
432     }
433
434     return $self;
435 }
436
437 # __END__ C4::OAI::ListIdentifiers
438
439 package C4::OAI::Description;
440
441 use strict;
442 use warnings;
443 use HTTP::OAI;
444 use HTTP::OAI::SAXHandler qw/ :SAX /;
445
446 sub new {
447     my ( $class, %args ) = @_;
448
449     my $self = {};
450
451     if(my $setDescription = $args{setDescription}) {
452         $self->{setDescription} = $setDescription;
453     }
454     if(my $handler = $args{handler}) {
455         $self->{handler} = $handler;
456     }
457
458     bless $self, $class;
459     return $self;
460 }
461
462 sub set_handler {
463     my ( $self, $handler ) = @_;
464
465     $self->{handler} = $handler if $handler;
466
467     return $self;
468 }
469
470 sub generate {
471     my ( $self ) = @_;
472
473     g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription});
474
475     return $self;
476 }
477
478 # __END__ C4::OAI::Description
479
480 package C4::OAI::ListSets;
481
482 use strict;
483 use warnings;
484 use HTTP::OAI;
485 use C4::OAI::Sets;
486
487 use base ("HTTP::OAI::ListSets");
488
489 sub new {
490     my ( $class, $repository, %args ) = @_;
491
492     my $self = HTTP::OAI::ListSets->new(%args);
493
494     my $token = C4::OAI::ResumptionToken->new(%args);
495     my $sets = GetOAISets;
496     my $pos = 0;
497     foreach my $set (@$sets) {
498         if ($pos < $token->{offset}) {
499             $pos++;
500             next;
501         }
502         my @descriptions;
503         foreach my $desc (@{$set->{'descriptions'}}) {
504             push @descriptions, C4::OAI::Description->new(
505                 setDescription => $desc,
506             );
507         }
508         $self->set(
509             HTTP::OAI::Set->new(
510                 setSpec => $set->{'spec'},
511                 setName => $set->{'name'},
512                 setDescription => \@descriptions,
513             )
514         );
515         $pos++;
516         last if ($pos + 1 - $token->{offset}) > $repository->{koha_max_count};
517     }
518
519     $self->resumptionToken(
520         new C4::OAI::ResumptionToken(
521             metadataPrefix => $token->{metadata_prefix},
522             offset         => $pos
523         )
524     ) if ( $pos > $token->{offset} );
525
526     return $self;
527 }
528
529 # __END__ C4::OAI::ListSets;
530
531 package C4::OAI::ListRecords;
532
533 use strict;
534 use warnings;
535 use C4::Biblio;
536 use HTTP::OAI;
537 use C4::OAI::Sets;
538 use MARC::File::XML;
539
540 use base ("HTTP::OAI::ListRecords");
541
542
543 sub new {
544     my ($class, $repository, %args) = @_;
545
546     my $self = HTTP::OAI::ListRecords->new(%args);
547
548     my $token = new C4::OAI::ResumptionToken( %args );
549     my $dbh = C4::Context->dbh;
550     my $set;
551     if(defined $token->{'set'}) {
552         $set = GetOAISetBySpec($token->{'set'});
553     }
554     my $max = $repository->{koha_max_count};
555     my $sql = "
556         (SELECT biblioitems.biblionumber, biblioitems.timestamp, marcxml
557         FROM biblioitems
558     ";
559     $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
560     $sql .= " WHERE timestamp >= ? AND timestamp <= ? ";
561     $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
562     $sql .= ") UNION
563         (SELECT deletedbiblio.biblionumber, null as marcxml, timestamp FROM deletedbiblio";
564     $sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
565     $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? ";
566     $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
567
568     $sql .= ") ORDER BY biblionumber
569         LIMIT " . ($max + 1) . "
570         OFFSET $token->{offset}
571     ";
572     my $sth = $dbh->prepare( $sql );
573     my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'});
574     push @bind_params, $set->{'id'} if defined $set;
575     push @bind_params, ($token->{'from'}, $token->{'until'});
576     push @bind_params, $set->{'id'} if defined $set;
577     $sth->execute( @bind_params );
578
579     my $count = 0;
580     my $format = $args{metadataPrefix} || $token->{metadata_prefix};
581     while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) {
582         $count++;
583         if ( $count > $max ) {
584             $self->resumptionToken(
585                 new C4::OAI::ResumptionToken(
586                     metadataPrefix  => $token->{metadata_prefix},
587                     from            => $token->{from},
588                     until           => $token->{until},
589                     offset          => $token->{offset} + $max,
590                     set             => $token->{set}
591                 )
592             );
593             last;
594         }
595         my $marcxml = $repository->get_biblio_marcxml($biblionumber, $format);
596         my $oai_sets = GetOAISetsBiblio($biblionumber);
597         my @setSpecs;
598         foreach (@$oai_sets) {
599             push @setSpecs, $_->{spec};
600         }
601         if ($marcxml) {
602           $self->record( C4::OAI::Record->new(
603               $repository, $marcxml, $timestamp, \@setSpecs,
604               identifier      => $repository->{ koha_identifier } . ':' . $biblionumber,
605               metadataPrefix  => $token->{metadata_prefix}
606           ) );
607         } else {
608           $self->record( C4::OAI::DeletedRecord->new(
609           $timestamp, \@setSpecs, identifier => $repository->{ koha_identifier } . ':' . $biblionumber ) );
610         }
611     }
612
613     # Return error if no results
614     unless ($count) {
615         return HTTP::OAI::Response->new(
616             requestURL => $repository->self_url(),
617             errors     => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ],
618         );
619     }
620
621     return $self;
622 }
623
624 # __END__ C4::OAI::ListRecords
625
626
627
628 package C4::OAI::Repository;
629
630 use base ("HTTP::OAI::Repository");
631
632 use strict;
633 use warnings;
634
635 use HTTP::OAI;
636 use HTTP::OAI::Repository qw/:validate/;
637
638 use XML::SAX::Writer;
639 use XML::LibXML;
640 use XML::LibXSLT;
641 use YAML::Syck qw( LoadFile );
642 use CGI qw/:standard -oldstyle_urls/;
643
644 use C4::Context;
645 use C4::Biblio;
646
647
648 sub new {
649     my ($class, %args) = @_;
650     my $self = $class->SUPER::new(%args);
651
652     $self->{ koha_identifier      } = C4::Context->preference("OAI-PMH:archiveID");
653     $self->{ koha_max_count       } = C4::Context->preference("OAI-PMH:MaxCount");
654     $self->{ koha_metadata_format } = ['oai_dc', 'marcxml'];
655     $self->{ koha_stylesheet      } = { }; # Build when needed
656
657     # Load configuration file if defined in OAI-PMH:ConfFile syspref
658     if ( my $file = C4::Context->preference("OAI-PMH:ConfFile") ) {
659         $self->{ conf } = LoadFile( $file );
660         my @formats = keys %{ $self->{conf}->{format} };
661         $self->{ koha_metadata_format } =  \@formats;
662     }
663
664     # Check for grammatical errors in the request
665     my @errs = validate_request( CGI::Vars() );
666
667     # Is metadataPrefix supported by the respository?
668     my $mdp = param('metadataPrefix') || '';
669     if ( $mdp && !grep { $_ eq $mdp } @{$self->{ koha_metadata_format }} ) {
670         push @errs, new HTTP::OAI::Error(
671             code    => 'cannotDisseminateFormat',
672             message => "Dissemination as '$mdp' is not supported",
673         );
674     }
675
676     my $response;
677     if ( @errs ) {
678         $response = HTTP::OAI::Response->new(
679             requestURL  => self_url(),
680             errors      => \@errs,
681         );
682     }
683     else {
684         my %attr = CGI::Vars();
685         my $verb = delete( $attr{verb} );
686         if ( $verb eq 'ListSets' ) {
687             $response = C4::OAI::ListSets->new($self, %attr);
688         }
689         elsif ( $verb eq 'Identify' ) {
690             $response = C4::OAI::Identify->new( $self );
691         }
692         elsif ( $verb eq 'ListMetadataFormats' ) {
693             $response = C4::OAI::ListMetadataFormats->new( $self );
694         }
695         elsif ( $verb eq 'GetRecord' ) {
696             $response = C4::OAI::GetRecord->new( $self, %attr );
697         }
698         elsif ( $verb eq 'ListRecords' ) {
699             $response = C4::OAI::ListRecords->new( $self, %attr );
700         }
701         elsif ( $verb eq 'ListIdentifiers' ) {
702             $response = C4::OAI::ListIdentifiers->new( $self, %attr );
703         }
704     }
705
706     $response->set_handler( XML::SAX::Writer->new( Output => *STDOUT ) );
707     $response->generate;
708
709     bless $self, $class;
710     return $self;
711 }
712
713
714 sub get_biblio_marcxml {
715     my ($self, $biblionumber, $format) = @_;
716     my $with_items = 0;
717     if ( my $conf = $self->{conf} ) {
718         $with_items = $conf->{format}->{$format}->{include_items};
719     }
720     my $record = GetMarcBiblio($biblionumber, $with_items, 1);
721     $record ? $record->as_xml() : undef;
722 }
723
724
725 sub stylesheet {
726     my ( $self, $format ) = @_;
727
728     my $stylesheet = $self->{ koha_stylesheet }->{ $format };
729     unless ( $stylesheet ) {
730         my $xsl_file = $self->{ conf }
731                        ? $self->{ conf }->{ format }->{ $format }->{ xsl_file }
732                        : ( C4::Context->config('intrahtdocs') .
733                          '/prog/en/xslt/' .
734                          C4::Context->preference('marcflavour') .
735                          'slim2OAIDC.xsl' );
736         my $parser = XML::LibXML->new();
737         my $xslt = XML::LibXSLT->new();
738         my $style_doc = $parser->parse_file( $xsl_file );
739         $stylesheet = $xslt->parse_stylesheet( $style_doc );
740         $self->{ koha_stylesheet }->{ $format } = $stylesheet;
741     }
742
743     return $stylesheet;
744 }
745
746
747
748 =head1 NAME
749
750 C4::OAI::Repository - Handles OAI-PMH requests for a Koha database.
751
752 =head1 SYNOPSIS
753
754   use C4::OAI::Repository;
755
756   my $repository = C4::OAI::Repository->new();
757
758 =head1 DESCRIPTION
759
760 This object extend HTTP::OAI::Repository object.
761 It accepts OAI-PMH HTTP requests and returns result.
762
763 This OAI-PMH server can operate in a simple mode and extended one.
764
765 In simple mode, repository configuration comes entirely from Koha system
766 preferences (OAI-PMH:archiveID and OAI-PMH:MaxCount) and the server returns
767 records in marcxml or dublin core format. Dublin core records are created from
768 koha marcxml records tranformed with XSLT. Used XSL file is located in
769 koha-tmpl/intranet-tmpl/prog/en/xslt directory and choosed based on marcflavour,
770 respecively MARC21slim2OAIDC.xsl for MARC21 and  MARC21slim2OAIDC.xsl for
771 UNIMARC.
772
773 In extende mode, it's possible to parameter other format than marcxml or Dublin
774 Core. A new syspref OAI-PMH:ConfFile specify a YAML configuration file which
775 list available metadata formats and XSL file used to create them from marcxml
776 records. If this syspref isn't set, Koha OAI server works in simple mode. A
777 configuration file koha-oai.conf can look like that:
778
779   ---
780   format:
781     vs:
782       metadataPrefix: vs
783       metadataNamespace: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs
784       schema: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs.xsd
785       xsl_file: /usr/local/koha/xslt/vs.xsl
786     marcxml:
787       metadataPrefix: marxml
788       metadataNamespace: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim
789       schema: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd
790     oai_dc:
791       metadataPrefix: oai_dc
792       metadataNamespace: http://www.openarchives.org/OAI/2.0/oai_dc/
793       schema: http://www.openarchives.org/OAI/2.0/oai_dc.xsd
794       xsl_file: /usr/local/koha/koha-tmpl/intranet-tmpl/xslt/UNIMARCslim2OAIDC.xsl
795
796 =cut
797
798
799