Bug 14045: DBRev 3.21.00.035
[koha.git] / opac / oai.pl
blob7eb0f6beffae8616d1abea35328542c902b8c6c3
1 #!/usr/bin/perl
3 # Copyright Biblibre 2008
5 # This file is part of Koha.
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.
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.
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>.
21 use strict;
22 use warnings;
24 use CGI qw( :standard -oldstyle_urls -utf8 );
25 use vars qw( $GZIP );
26 use C4::Context;
29 BEGIN {
30 eval { require PerlIO::gzip };
31 $GZIP = ($@) ? 0 : 1;
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',
41 "OAI-PMH service is disabled";
42 exit;
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',
52 binmode( STDOUT, ":gzip" );
54 else {
55 print header(
56 -type => 'text/xml; charset=utf-8',
57 -charset => 'utf-8',
61 binmode STDOUT, ':encoding(UTF-8)';
62 my $repository = C4::OAI::Repository->new();
67 # __END__ Main Prog
71 # Extends HTTP::OAI::ResumptionToken
72 # A token is identified by:
73 # - metadataPrefix
74 # - from
75 # - until
76 # - offset
78 package C4::OAI::ResumptionToken;
80 use strict;
81 use warnings;
82 use HTTP::OAI;
84 use base ("HTTP::OAI::ResumptionToken");
87 sub new {
88 my ($class, %args) = @_;
90 my $self = $class->SUPER::new(%args);
92 my ($metadata_prefix, $offset, $from, $until, $set);
93 if ( $args{ resumptionToken } ) {
94 ($metadata_prefix, $offset, $from, $until, $set)
95 = split( '/', $args{resumptionToken} );
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 );
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} || '';
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);
120 $self->resumptionToken(
121 join( '/', $metadata_prefix, $offset, $from, $until, $set ) );
122 $self->cursor( $offset );
124 return $self;
127 sub _strip_UTC_designators {
128 my ( $timestamp ) = @_;
129 $timestamp =~ s/T/ /g;
130 $timestamp =~ s/Z//g;
131 return $timestamp;
134 # __END__ C4::OAI::ResumptionToken
138 package C4::OAI::Identify;
140 use strict;
141 use warnings;
142 use HTTP::OAI;
143 use C4::Context;
145 use base ("HTTP::OAI::Identify");
147 sub new {
148 my ($class, $repository) = @_;
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',
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" );
167 $self->compression( 'gzip' );
169 return $self;
172 # __END__ C4::OAI::Identify
176 package C4::OAI::ListMetadataFormats;
178 use strict;
179 use warnings;
180 use HTTP::OAI;
182 use base ("HTTP::OAI::ListMetadataFormats");
184 sub new {
185 my ($class, $repository) = @_;
187 my $self = $class->SUPER::new();
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}, ) );
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 ) );
211 return $self;
214 # __END__ C4::OAI::ListMetadataFormats
218 package C4::OAI::Record;
220 use strict;
221 use warnings;
222 use HTTP::OAI;
223 use HTTP::OAI::Metadata::OAI_DC;
225 use base ("HTTP::OAI::Record");
227 sub new {
228 my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_;
230 my $self = $class->SUPER::new(%args);
232 $timestamp =~ s/ /T/, $timestamp .= 'Z';
233 $self->header( new HTTP::OAI::Header(
234 identifier => $args{identifier},
235 datestamp => $timestamp,
236 ) );
238 foreach my $setSpec (@$setSpecs) {
239 $self->header->setSpec($setSpec);
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') . "'"
249 $record_dom = $repository->stylesheet($format)->transform($record_dom, %args);
251 $self->metadata( HTTP::OAI::Metadata->new( dom => $record_dom ) );
253 return $self;
256 # __END__ C4::OAI::Record
258 package C4::OAI::DeletedRecord;
260 use Modern::Perl;
261 use HTTP::OAI;
262 use HTTP::OAI::Metadata::OAI_DC;
264 use base ("HTTP::OAI::Record");
266 sub new {
267 my ($class, $timestamp, $setSpecs, %args) = @_;
269 my $self = $class->SUPER::new(%args);
271 $timestamp =~ s/ /T/, $timestamp .= 'Z';
272 $self->header( new HTTP::OAI::Header(
273 status => 'deleted',
274 identifier => $args{identifier},
275 datestamp => $timestamp,
276 ) );
278 foreach my $setSpec (@$setSpecs) {
279 $self->header->setSpec($setSpec);
282 return $self;
285 # __END__ C4::OAI::DeletedRecord
289 package C4::OAI::GetRecord;
291 use strict;
292 use warnings;
293 use HTTP::OAI;
294 use C4::Biblio;
295 use C4::OAI::Sets;
296 use MARC::File::XML;
298 use base ("HTTP::OAI::GetRecord");
301 sub new {
302 my ($class, $repository, %args) = @_;
304 my $self = HTTP::OAI::GetRecord->new(%args);
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 ))
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 ) ],
329 else {
330 $deleted = 1;
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};
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)
351 return $self;
354 # __END__ C4::OAI::GetRecord
358 package C4::OAI::ListIdentifiers;
360 use strict;
361 use warnings;
362 use HTTP::OAI;
363 use C4::OAI::Sets;
365 use base ("HTTP::OAI::ListIdentifiers");
368 sub new {
369 my ($class, $repository, %args) = @_;
371 my $self = HTTP::OAI::ListIdentifiers->new(%args);
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'});
379 my $max = $repository->{koha_max_count};
380 my $sql = "
381 (SELECT biblioitems.biblionumber, biblioitems.timestamp
382 FROM biblioitems
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;
393 $sql .= ") ORDER BY biblionumber
394 LIMIT " . ($max+1) . "
395 OFFSET $token->{offset}
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 );
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}
417 last;
419 $timestamp =~ s/ /T/, $timestamp .= 'Z';
420 $self->identifier( new HTTP::OAI::Header(
421 identifier => $repository->{ koha_identifier} . ':' . $biblionumber,
422 datestamp => $timestamp,
423 ) );
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' ) ],
434 return $self;
437 # __END__ C4::OAI::ListIdentifiers
439 package C4::OAI::Description;
441 use strict;
442 use warnings;
443 use HTTP::OAI;
444 use HTTP::OAI::SAXHandler qw/ :SAX /;
446 sub new {
447 my ( $class, %args ) = @_;
449 my $self = {};
451 if(my $setDescription = $args{setDescription}) {
452 $self->{setDescription} = $setDescription;
454 if(my $handler = $args{handler}) {
455 $self->{handler} = $handler;
458 bless $self, $class;
459 return $self;
462 sub set_handler {
463 my ( $self, $handler ) = @_;
465 $self->{handler} = $handler if $handler;
467 return $self;
470 sub generate {
471 my ( $self ) = @_;
473 g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription});
475 return $self;
478 # __END__ C4::OAI::Description
480 package C4::OAI::ListSets;
482 use strict;
483 use warnings;
484 use HTTP::OAI;
485 use C4::OAI::Sets;
487 use base ("HTTP::OAI::ListSets");
489 sub new {
490 my ( $class, $repository, %args ) = @_;
492 my $self = HTTP::OAI::ListSets->new(%args);
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;
502 my @descriptions;
503 foreach my $desc (@{$set->{'descriptions'}}) {
504 push @descriptions, C4::OAI::Description->new(
505 setDescription => $desc,
508 $self->set(
509 HTTP::OAI::Set->new(
510 setSpec => $set->{'spec'},
511 setName => $set->{'name'},
512 setDescription => \@descriptions,
515 $pos++;
516 last if ($pos + 1 - $token->{offset}) > $repository->{koha_max_count};
519 $self->resumptionToken(
520 new C4::OAI::ResumptionToken(
521 metadataPrefix => $token->{metadata_prefix},
522 offset => $pos
524 ) if ( $pos > $token->{offset} );
526 return $self;
529 # __END__ C4::OAI::ListSets;
531 package C4::OAI::ListRecords;
533 use strict;
534 use warnings;
535 use C4::Biblio;
536 use HTTP::OAI;
537 use C4::OAI::Sets;
538 use MARC::File::XML;
540 use base ("HTTP::OAI::ListRecords");
543 sub new {
544 my ($class, $repository, %args) = @_;
546 my $self = HTTP::OAI::ListRecords->new(%args);
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'});
554 my $max = $repository->{koha_max_count};
555 my $sql = "
556 (SELECT biblioitems.biblionumber, biblioitems.timestamp, marcxml
557 FROM biblioitems
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;
568 $sql .= ") ORDER BY biblionumber
569 LIMIT " . ($max + 1) . "
570 OFFSET $token->{offset}
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 );
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}
593 last;
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};
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 ) );
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' ) ],
621 return $self;
624 # __END__ C4::OAI::ListRecords
628 package C4::OAI::Repository;
630 use base ("HTTP::OAI::Repository");
632 use strict;
633 use warnings;
635 use HTTP::OAI;
636 use HTTP::OAI::Repository qw/:validate/;
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/;
644 use C4::Context;
645 use C4::Biblio;
648 sub new {
649 my ($class, %args) = @_;
650 my $self = $class->SUPER::new(%args);
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
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;
664 # Check for grammatical errors in the request
665 my @errs = validate_request( CGI::Vars() );
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",
676 my $response;
677 if ( @errs ) {
678 $response = HTTP::OAI::Response->new(
679 requestURL => self_url(),
680 errors => \@errs,
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);
689 elsif ( $verb eq 'Identify' ) {
690 $response = C4::OAI::Identify->new( $self );
692 elsif ( $verb eq 'ListMetadataFormats' ) {
693 $response = C4::OAI::ListMetadataFormats->new( $self );
695 elsif ( $verb eq 'GetRecord' ) {
696 $response = C4::OAI::GetRecord->new( $self, %attr );
698 elsif ( $verb eq 'ListRecords' ) {
699 $response = C4::OAI::ListRecords->new( $self, %attr );
701 elsif ( $verb eq 'ListIdentifiers' ) {
702 $response = C4::OAI::ListIdentifiers->new( $self, %attr );
706 $response->set_handler( XML::SAX::Writer->new( Output => *STDOUT ) );
707 $response->generate;
709 bless $self, $class;
710 return $self;
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};
720 my $record = GetMarcBiblio($biblionumber, $with_items, 1);
721 $record ? $record->as_xml() : undef;
725 sub stylesheet {
726 my ( $self, $format ) = @_;
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;
743 return $stylesheet;
748 =head1 NAME
750 C4::OAI::Repository - Handles OAI-PMH requests for a Koha database.
752 =head1 SYNOPSIS
754 use C4::OAI::Repository;
756 my $repository = C4::OAI::Repository->new();
758 =head1 DESCRIPTION
760 This object extend HTTP::OAI::Repository object.
761 It accepts OAI-PMH HTTP requests and returns result.
763 This OAI-PMH server can operate in a simple mode and extended one.
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.
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:
780 format:
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
796 =cut