From 2aa490118c95457c1f3c3f75ec7bc4036f5e7b10 Mon Sep 17 00:00:00 2001 From: Lucien van Wouw Date: Thu, 2 Nov 2017 15:14:51 +0100 Subject: [PATCH] LP#1729620 New optional feature: an OAI2 provider service. This module is an opensrf service that exposes bibliographic and authority records through the OAI2 protocol. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. Signed-off-by: Lucien van Wouw, IISG, Amsterdam. Signed-off-by: Remington Steed Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton Signed-off-by: Mike Rylander --- .gitignore | 1 + Open-ILS/examples/apache_24/eg_vhost.conf.in | 9 + Open-ILS/examples/fm_IDL.xml | 22 + Open-ILS/examples/opensrf.xml.example | 113 ++++- Open-ILS/examples/opensrf_core.xml.example | 1 + .../src/perlmods/lib/OpenILS/Application/OAI.pm | 520 +++++++++++++++++++++ Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm | 478 +++++++++++++++++++ Open-ILS/src/sql/Pg/oai.sql | 68 +++ Open-ILS/xsl/OAI2_MARC21slim.xsl | 18 + Open-ILS/xsl/OAI2_OAIDC.xsl | 13 + docs/RELEASE_NOTES_NEXT/OAI2/install.adoc | 408 ++++++++++++++++ 11 files changed, 1650 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm create mode 100644 Open-ILS/src/sql/Pg/oai.sql create mode 100644 Open-ILS/xsl/OAI2_MARC21slim.xsl create mode 100644 Open-ILS/xsl/OAI2_OAIDC.xsl create mode 100644 docs/RELEASE_NOTES_NEXT/OAI2/install.adoc diff --git a/.gitignore b/.gitignore index 23d7d99b32..b49215d978 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.pyc *.slo *.class +.idea/ aclocal.m4 autom4te.cache/ diff --git a/Open-ILS/examples/apache_24/eg_vhost.conf.in b/Open-ILS/examples/apache_24/eg_vhost.conf.in index 376aca9e2f..d62ca3bd98 100644 --- a/Open-ILS/examples/apache_24/eg_vhost.conf.in +++ b/Open-ILS/examples/apache_24/eg_vhost.conf.in @@ -88,6 +88,15 @@ OSRFTranslatorConfig @sysconfdir@/opensrf_core.xml Require all granted +# Uncomment this section to enable the OAI2 provider service. +# +# SetHandler perl-script +# PerlHandler OpenILS::WWW::OAI +# Options +ExecCGI +# PerlSendHeader On +# Require all granted +# + # ---------------------------------------------------------------------------------- # Replace broken cover images with a transparent GIF by default # ---------------------------------------------------------------------------------- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index e6e4c6a6af..4880eeec7a 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -74,6 +74,28 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index 24e9005b72..33e14e7d10 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -991,6 +991,116 @@ vim:et:ts=4:sw=4: + + 5 + 1 + perl + OpenILS::Application::OAI + 199 + + open-ils.oai_unix.sock + open-ils.oai_unix.pid + 1000 + open-ils.oai_unix.log + 1 + 5 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6 1 @@ -1364,7 +1474,8 @@ vim:et:ts=4:sw=4: open-ils.storage open-ils.justintime open-ils.cstore - open-ils.collections + open-ils.collections + open-ils.oai open-ils.qstore open-ils.reporter open-ils.reporter-store diff --git a/Open-ILS/examples/opensrf_core.xml.example b/Open-ILS/examples/opensrf_core.xml.example index aeba4ad94c..3c0d366ac6 100644 --- a/Open-ILS/examples/opensrf_core.xml.example +++ b/Open-ILS/examples/opensrf_core.xml.example @@ -30,6 +30,7 @@ Example OpenSRF bootstrap configuration file for Evergreen open-ils.courses open-ils.curbside open-ils.fielder + openils.oai open-ils.pcrud open-ils.permacrud open-ils.reporter diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm new file mode 100644 index 0000000000..c4fb61a8ef --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm @@ -0,0 +1,520 @@ +# OpenILS::WWW::OAI manages OAI2 requests and responses. +# +# Copyright (c) 2014-2017 International Institute of Social History +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# +# Author: Lucien van Wouw + + +package OpenILS::Application::OAI; +use strict; use warnings; + +use base qw/OpenILS::Application/; +use OpenSRF::AppSession; +use OpenSRF::EX qw(:try); +use MARC::Record; +use MARC::File::XML ( BinaryEncoding => 'UTF-8' ); +use OpenSRF::Utils::SettingsClient; +use OpenSRF::Utils::Logger qw($logger); +use XML::LibXML; +use XML::LibXSLT; + +my ( + $_parser, + $_xslt, + %record_xslt, + %metarecord_xslt, + %holdings_data_cache, + %authority_browse_axis_cache, + %copies, + $barcode_filter, + $status_filter +); + + +sub child_init { + + # set the XML parser + $_parser = new XML::LibXML; + + # and the xslt parser + $_xslt = new XML::LibXSLT; + + # Load the metadataformats that are configured. + my $metadata_format = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'metadataformat'}; + if ( $metadata_format ) { + for my $schema ( keys %$metadata_format ) { + $logger->info('Loading schema ' . $schema) ; + $record_xslt{$schema}{namespace_uri} = $metadata_format->{$schema}->{namespace_uri}; + $record_xslt{$schema}{schema_location} = $metadata_format->{$schema}->{schema_location}; + $record_xslt{$schema}{xslt} = $_xslt->parse_stylesheet( $_parser->parse_file( + OpenSRF::Utils::SettingsClient->new->config_value( dirs => 'xsl' ) . '/' . $metadata_format->{$schema}->{xslt} + ) ); + } + } + + # Fall back on system defaults if oai_dc is not set in the configuration. + unless ( exists $record_xslt{oai_dc} ) { + $logger->info('Loading default oai_dc schema') ; + my $xslt = $_parser->parse_file( + OpenSRF::Utils::SettingsClient + ->new + ->config_value( dirs => 'xsl' ). + "/OAI2_OAIDC.xsl" + ); + # and stash a transformer + $record_xslt{oai_dc}{xslt} = $_xslt->parse_stylesheet( $xslt ); + $record_xslt{oai_dc}{namespace_uri} = 'http://www.openarchives.org/OAI/2.0/oai_dc/'; + $record_xslt{oai_dc}{schema_location} = 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd'; + } + + # If not defined, use the natural marcxml metadata setting + unless ( exists $record_xslt{marcxml}) { + $logger->info('Loading default marcxml schema') ; + my $xslt = $_parser->parse_file( + OpenSRF::Utils::SettingsClient + ->new + ->config_value( dirs => 'xsl' ). + "/OAI2_MARC21slim.xsl" + ); + # and stash a transformer + $record_xslt{marcxml}{xslt} = $_xslt->parse_stylesheet( $xslt ); + $record_xslt{marcxml}{namespace_uri} = 'http://www.loc.gov/MARC21/slim'; + $record_xslt{marcxml}{docs} = 'http://www.loc.gov/MARC21/slim'; + $record_xslt{marcxml}{schema_location} = 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd'; + } + + # Load the mapping of 852 holdings. + my $copies = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'copies'} ; + if ( $copies ) { + foreach my $subfield_code (keys %$copies) { + my $value = $copies->{$subfield_code}; + $logger->info('Set 852 map ' . $subfield_code . '=' . $value ); + $copies{$subfield_code} = $value; + } + } else { # if not defined, fall back on these defaults. + %copies = ( + a => 'location', + b => 'owning_lib', + c => 'callnumber', + d => 'circlib', + g => 'barcode', + n => 'status' + ); + } + + # Set the barcode filter and status filter + $barcode_filter = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'barcode_filter'}; + $status_filter = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}->{'status_filter'}; + + return 1; +} + + +sub list_record_formats { + + my @list; + for my $type ( keys %record_xslt ) { + push @list, + { $type => + { namespace_uri => $record_xslt{$type}{namespace_uri}, + docs => $record_xslt{$type}{docs}, + schema_location => $record_xslt{$type}{schema_location}, + } + }; + } + + return \@list; +} + +__PACKAGE__->register_method( + method => 'list_record_formats', + api_name => 'open-ils.oai.record.formats', + api_level => 1, + argc => 0, + signature => + { + desc => 'Returns the list of valid record formats that oai understands.', + 'return' => + { + desc => 'The format list.', + type => 'array' + } + } +); + + +sub oai_biblio_retrieve { + + my $self = shift; + my $client = shift; + my $tcn = shift; + my $metadataPrefix = shift; + + # holdings hold an array of call numbers, which hold an array of copies + # holdings => [ label: { library, [ copies: { barcode, location, status, circ_lib } ] } ] + my %holdings; + + my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' ); + + # Retrieve the bibliographic record and it's copies + my $tree = $_storage->request( + "open-ils.cstore.direct.biblio.record_entry.retrieve", + $tcn, + { flesh => 5, + flesh_fields => { + bre => [qw/marc edit_date call_numbers/], + acn => [qw/edit_date copies owning_lib prefix suffix/], + acp => [qw/edit_date location status circ_lib parts/], + } + } + )->gather(1); + + # Create a MARC::Record object with the marc. + my $marc = MARC::Record->new_from_xml( $tree->marc, 'UTF8', 'XML'); + + # Retrieve the MFHD where we can find them. + my %serials; + if ( substr($marc->leader, 7, 1) eq 's' ) { # serial + my $_search = OpenSRF::AppSession->create( 'open-ils.search' ); + my $_serials = $_search->request('open-ils.search.serial.record.bib.retrieve', $tcn, 1, 0)->gather(1); + my $order = 0 ; + for my $sre (@$_serials) { + if ( $sre->location ) { + $order++ ; + my @svr = split( ' -- ', $sre->location ); + my $cn_label = $svr[-1]; + $serials{$order}{'label'} = $cn_label ; + my $display = @{$sre->basic_holdings_add} ? $sre->basic_holdings_add : $sre->basic_holdings; + $serials{$order}{'ser'} = join(', ', @{$display}); + } + } + } + + my $edit_date = $tree->edit_date ; + + # Prepare a hash of all holdings and serials + for my $cn (@{$tree->call_numbers}) { + + next unless ( $cn->deleted eq 'f' || !$cn->deleted ); + my $_visible = 0; + for my $c (@{$cn->copies}) { + $_visible = _cp_is_visible($cn, $c); + last if ( $_visible ); + } + next unless $_visible; + + my $cn_label = $cn->label; + $holdings{$cn_label}{'owning_lib'} = $cn->owning_lib->shortname; + + $edit_date = most_recent_date( $cn->edit_date, $edit_date ); + + for my $cp (@{$cn->copies}) { + + next unless _cp_is_visible($cn, $cp); + $edit_date = most_recent_date( $cp->edit_date, $edit_date ); + + # find the corresponding serial. + # There is no way of knowing here if the barcode 852$p is a correct match. + my $order = 0 ; + my $ser; + foreach my $key (sort keys %serials) { + my $serial = $serials{$key}; + if ( $serial->{'label'} eq $cn_label ) { + $ser = $serial->{'ser'}; + $order = $key; + delete $serials{$key}; # in case we have several serial holdings with the same call number + last; + } + } + $holdings{$cn_label}{'order'} = $order ; + + my $circlib = $cp->circ_lib->shortname ; + push @{$holdings{$cn->label}{'copies'}}, { + owning_lib => $cn->owning_lib->shortname, + callnumber => $cn->label, + barcode => $cp->barcode, + status => $cp->status->name, + location => $cp->location->name, + circlib => $circlib, + ser => $ser + }; + } + } + + ## Append the holdings and MFHD data to the marc record and apply the stylesheet. + if ( %holdings ) { + + # Force record leader to 'a' as our data is always UTF8 + # Avoids marc8_to_utf8 from being invoked with horrible results + # on the off-chance the record leader isn't correct + my $ldr = $marc->leader; + substr($ldr, 9, 1, 'a'); + $marc->leader($ldr); + + # Expects the record ID in the 001 + $marc->delete_field($_) for ($marc->field('001')); + if (!$marc->field('001')) { + $marc->insert_fields_ordered( + MARC::Field->new( '001', $tcn ) + ); + } + + # Our reference node to prepend nodes to. + my $reference = $marc->field('901'); + + $marc->delete_field($_) for ($marc->field('852')); # remove any legacy 852s + foreach my $cn (sort { $holdings{$a}->{'order'} <=> $holdings{$b}->{'order'}} keys %holdings) { + foreach my $cp (@{$holdings{$cn}->{'copies'}}) { + my $marc_852 = MARC::Field->new( + '852', '4', ' ', 0 => 'dummy'); # The dummy is necessary to prevent a validation error. + foreach my $subfield_code (sort keys %copies) { + my $_cp = $copies{$subfield_code} ; + $marc_852->add_subfields($subfield_code, $cp->{$_cp} || $_cp) if ($_cp); + } + $marc_852->delete_subfield(code => '0'); + $marc->insert_fields_before($reference, $marc_852); + if ( $cp->{'ser'} ) { + my $marc_866_a = MARC::Field->new( '866', '4', ' ', 'a' => $cp->{'ser'}); + $marc->insert_fields_after( $marc_852, $marc_866_a ) ; + } + } + } + + } + + my $xslt = $record_xslt{$metadataPrefix}{xslt} ; + my $xml = $xslt->transform( $_parser->parse_string( $marc->as_xml_record()) ); + return $xslt->output_as_chars( $xml ) ; +} + + +sub most_recent_date { + + my $date1 = substr(shift, 0, 19) ; # e.g. '2001-02-03T04:05:06+0000' becomes '2001-02-03T04:05:06' + my $date2 = substr(shift, 0, 19) ; + my $_date1 = $date1 ; + my $_date2 = $date2 ; + + $date1 =~ s/[-T:\.\+]//g ; # '2001-02-03T04:05:06' becomes '20010203040506' + $date2 =~ s/[-T:\.\+]//g ; + + return $_date1 if ( $date1 > $date2) ; + return $_date2 ; +} + + +sub _cp_is_visible { + + my $cn = shift; + my $cp = shift; + + my $visible = 0; + if ( ($cp->deleted eq 'f' || !$cp->deleted) && + ( ! $barcode_filter || $cp->barcode =~ /$barcode_filter/ ) && + $cp->location->opac_visible eq 't' && + $cp->status->opac_visible eq 't' && + $cp->opac_visible eq 't' && + $cp->circ_lib->opac_visible eq 't' && + $cn->owning_lib->opac_visible eq 't' && + (! $status_filter || $cp->status->name =~ /$status_filter/ ) + ) { + $visible = 1; + } + + return $visible; +} + +__PACKAGE__->register_method( + method => 'oai_biblio_retrieve', + api_name => 'open-ils.oai.biblio.retrieve', + api_level => 1, + argc => 1, + signature => + { + desc => 'Returns the MARCXML representation of the requested bibliographic record.', + params => + [ + { + name => 'tcn', + desc => 'An OpenILS biblio::record_entry id.', + type => 'number' + }, + { + name => 'metadataPrefix', + desc => 'The metadataPrefix of the schema.', + type => 'string' + } + ], + 'return' => + { + desc => 'An string of the XML in the desired schema.', + type => 'string' + } + } +); + + +sub oai_authority_retrieve { + + my $self = shift; + my $client = shift; + my $tcn = shift; + my $metadataPrefix = shift; + + my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' ); + + # Retrieve the authority record + my $record = $_storage->request('open-ils.cstore.direct.authority.record_entry.retrieve', $tcn)->gather(1); + my $o = Fieldmapper::authority::record_entry->new($record) ; + my $marc = MARC::Record->new_from_xml( $o->marc, 'UTF8', 'XML'); + + # Expects the record ID in the 001 + $marc->delete_field($_) for ($marc->field('001')); + if (!$marc->field('001')) { + $marc->insert_fields_ordered( + MARC::Field->new( '001', $tcn ) + ); + } + + my $xslt = $record_xslt{$metadataPrefix}{xslt} ; + my $xml = $record_xslt{$metadataPrefix}{xslt}->transform( + $_parser->parse_string( $marc->as_xml_record()) + ); + return $record_xslt{$metadataPrefix}{xslt}->output_as_chars( $xml ) ; +} + + +__PACKAGE__->register_method( + method => 'oai_authority_retrieve', + api_name => 'open-ils.oai.authority.retrieve', + api_level => 1, + argc => 1, + signature => + { + desc => 'Returns the MARCXML representation of the requested authority record.', + params => + [ + { + name => 'tcn', + desc => 'An OpenILS authority::record_entry id.', + type => 'number' + }, + { + name => 'metadataPrefix', + desc => 'The metadataPrefix of the schema.', + type => 'string' + } + ], + 'return' => + { + desc => 'An string of the XML in the desired schema.', + type => 'string' + } + } +); + + +sub oai_list_retrieve { + + my $self = shift; + my $client = shift; + my $record_class = shift || 'biblio'; + my $tcn = shift || 0; + my $from = shift; + my $until = shift; + my $set = shift ; + my $metadataPrefix = shift; + my $max_count = shift; + my $deleted_record = shift || 'yes'; + + my $query = {}; + $query->{'tcn'} = ($max_count eq 1) ? $tcn : {'>=' => $tcn} ; + $query->{'set_spec'} = $set if ( $set ); # unsupported + $query->{'deleted'} = 'f' unless ( $deleted_record eq 'yes' ); + $query->{'datestamp'} = {'>=', $from} if ( $from && !$until ) ; + $query->{'datestamp'} = {'<=', $until} if ( !$from && $until ) ; + $query->{'-and'} = [{'datestamp'=>{'>=' => $from}}, {'datestamp'=>{'<=' => $until}}] if ( $from && $until ) ; + + my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' ); + return $_storage->request('open-ils.cstore.direct.oai.' . $record_class . '.search.atomic', + $query, + { + limit => $max_count + 1 + } + )->gather(1); +} + +__PACKAGE__->register_method( + method => 'oai_list_retrieve', + api_name => 'open-ils.oai.list.retrieve', + api_level => 1, + argc => 1, + signature => + { + desc => 'Returns a list of record identifiers.', + params => + [ + { + name => 'record_class', + desc => '\'biblio\' for bibliographic records or \'authority\' for authority records', + type => 'string' + }, { + name => 'tcn', + desc => 'An optional tcn number used as a cursor.', + type => 'number' + }, + { + name => 'from', + desc => 'The datestamp the resultset range should begin with.', + type => 'string' + }, + { + name => 'until', + desc => 'The datestamp the resultset range should end with.', + type => 'string' + }, + { + name => 'set', + desc => 'A setspec.', + type => 'string' + }, + { + name => 'metadataPrefix', + desc => 'The metadataPrefix of the schema.', + type => 'string' + }, + { + name => 'offset', + desc => 'The start of the cursor position in the result set.', + type => 'number' + }, + { + name => 'deleted_record', + desc => 'If set to \'no\' the response will only include active records.', + type => 'string' + } + ], + 'return' => + { + desc => 'An OAI type record.', + type => 'array' + } + } +); + + +1; \ No newline at end of file diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm new file mode 100644 index 0000000000..7fc5f36ab3 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm @@ -0,0 +1,478 @@ +# OpenILS::WWW::OAI manages OAI2 requests and responses. +# +# Copyright (c) 2014-2017 International Institute of Social History +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# +# Author: Lucien van Wouw + + +package OpenILS::WWW::OAI; +use strict; use warnings; +use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log); +use CGI; +use DateTime::Format::ISO8601; +use HTTP::OAI; +use HTTP::OAI::Metadata::OAI_Identifier; +use HTTP::OAI::Repository qw/:validate/; +use MARC::File::XML ( BinaryEncoding => 'UTF-8' ); +use MARC::Record; +use MIME::Base64; +use OpenSRF::EX qw(:try); +use OpenSRF::Utils::Logger qw/$logger/; +use XML::LibXML; +use XML::LibXSLT; + +my ( + $bootstrap, + $base_url, + $repository_identifier, + $repository_name, + $admin_email, + $earliest_datestamp, + $deleted_record, + $max_count, + $granularity, + $scheme, + $delimiter, + $sample_identifier, + $list_sets, + $oai_metadataformats, + $oai_sets, + $oai, + $parser, + $xslt +); + + +sub import { + + my $self = shift; + $bootstrap = shift; +} + + +sub child_init { + + OpenSRF::System->bootstrap_client( config_file => $bootstrap ); + + my $idl = OpenSRF::Utils::SettingsClient->new->config_value('IDL'); + Fieldmapper->import(IDL => $idl); + + $oai = OpenSRF::AppSession->create('open-ils.oai'); + $parser = new XML::LibXML; + $xslt = new XML::LibXSLT; + + my $app_settings = OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.oai')->{'app_settings'}; + $base_url = $app_settings->{'base_url'} || 'localhost'; + $base_url =~/(.*)\/$/ ; # Keep all minus the trailing forward slash. + $repository_identifier = $app_settings->{'repository_identifier'} || 'localhost'; + $repository_name = $app_settings->{'repository_name '} || 'A name'; + $admin_email = $app_settings->{'admin_email'} || 'adminEmail@' . $repository_identifier ; + $earliest_datestamp = $app_settings->{'earliest_datestamp'} || '0001-01-01' ; + $deleted_record = $app_settings->{'deleted_record'} || 'yes' ; + $max_count = $app_settings->{'max_count'} || 50; + $granularity = $app_settings->{'granularity' } || 'YYYY-MM-DDThh:mm:ss'; + $scheme = $app_settings->{'scheme'} || 'oai'; + $delimiter = $app_settings->{'delimiter'} || ':'; + $sample_identifier = $app_settings->{'sample_identifier'} || $scheme . $delimiter . $repository_identifier . $delimiter . '12345' ; + $list_sets = $app_settings->{'list_sets'} || 0; + + if ( $list_sets ) { + _load_oaisets_authority(); + _load_oaisets_biblio(); + } + _load_oai_metadataformats(); + + return Apache2::Const::OK; +} + + +sub handler { + + my $apache = shift; + return Apache2::Const::DECLINED if (-e $apache->filename); + + unless (defined $oai) { + $logger->error('Application session variables not defined. Add \'PerlChildInitHandler OpenILS::WWW::OAI::child_init\' to the Apache virtual host configuration file.'); + child_init(); + } + + my $cgi = new CGI; + my $record_class; + if ( $cgi->path_info =~ /\/(authority|biblio)/ ) { + $record_class = $1 ; + } else { + return Apache2::Const::NOT_FOUND ; + } + + my %attr = $cgi->Vars(); + my $requestURL = $base_url + . '/' . $record_class + . '?' + . join('&', map { "$_=$attr{$_}" } keys %attr); + $logger->info('Request url=' . $requestURL ) ; + + my $response; + my @errors = validate_request( %attr ); + if ( !@errors ) { + + # Retrieve our parameters + my $verb = delete( $attr{verb} ); + my $identifier = $attr{identifier}; + my $metadataPrefix = $attr{metadataPrefix} ; + my $from = $attr{from}; + my $until = $attr{'until'}; + my $set = $attr{set}; + my $resumptionToken = decode_base64($attr{resumptionToken} ) if $attr{resumptionToken}; + my $offset = 0 ; + if ( $resumptionToken ) { + ($metadataPrefix, $from, $until, $set, $offset) = split( '\$', $resumptionToken ); + } + + # Is the set valid ? + if ( $set ) { + my $_set = $oai_sets->{$set}; + if ( $_set && $_set->{id} && $_set->{record_class} eq $record_class) { + $set = $_set->{id} ; + } else { + push @errors, new HTTP::OAI::Error(code=>'noRecordsMatch', message=>"Set argument doesn't match any sets. The setSpec was '$set'") ; + } + } + + # Are the from and until ranges aligned ? + if ( $from && $until ) { + my $_from = $from ; + my $_until = $until ; + $_from =~ s/[-T:\.\+Z]//g ; # '2001-02-03T04:05:06Z' becomes '20010203040506' + $_until =~ s/[-T:\.\+Z]//g ; + push @errors, new HTTP::OAI::Error(code=>'badArgument', message=>'Bad date values, must have from<=until') unless ($_from <= $_until); + } + + # Is this metadataformat available ? + push @errors, new HTTP::OAI::Error(code=>'cannotDisseminateFormat', message=>'The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository') unless ( ($verb eq 'ListMetadataFormats' || $verb eq 'ListSets' || $verb eq 'Identify') || $oai_metadataformats->{$metadataPrefix} ); + + if ( !@errors ) { + + # Now prepare the response + if ( $verb eq 'ListRecords' ) { + $response = listRecords( $record_class, $requestURL, $from, $until, $set, $metadataPrefix, $offset); + } + elsif ( $verb eq 'ListMetadataFormats' ) { + $response = listMetadataFormats(); + } + elsif ( $verb eq 'ListSets' ) { + $response = listSets( $record_class, $requestURL ); + } + elsif ( $verb eq 'GetRecord' ) { + $response = getRecord( $record_class, $requestURL, $identifier, $metadataPrefix); + } + elsif ( $verb eq 'ListIdentifiers' ) { + $response = listIdentifiers( $record_class, $requestURL, $from, $until, $set, $metadataPrefix, $offset); + } + else { # Identify + $response = identify($record_class); + } + } + } + + if ( @errors ) { + $response = HTTP::OAI::Response->new( requestURL => $requestURL ); + $response->errors(@errors); + } + + $cgi->header(-type=>'text/xml', -charset=>'utf-8'); + $cgi->print($response->toDOM->toString()); + + return Apache2::Const::OK; +} + + +sub identify { + + my $record_class = shift; + + my $response = HTTP::OAI::Identify->new( + protocolVersion => '2.0', + baseURL => $base_url . '/' . $record_class, + repositoryName => $repository_name, + adminEmail => $admin_email, + MaxCount => $max_count, + granularity => $granularity, + earliestDatestamp => $earliest_datestamp, + deletedRecord => $deleted_record + ); + + $response->description( + HTTP::OAI::Metadata::OAI_Identifier->new( + 'scheme', $scheme, + 'repositoryIdentifier' , $repository_identifier, + 'delimiter', $delimiter, + 'sampleIdentifier', $sample_identifier + ) + ); + + return $response; +} + + +sub listMetadataFormats { + + my $response = HTTP::OAI::ListMetadataFormats->new(); + foreach my $metadataPrefix (keys $oai_metadataformats) { + my $metadata_format = $oai_metadataformats->{$metadataPrefix} ; + $response->metadataFormat( HTTP::OAI::MetadataFormat->new( + metadataPrefix => $metadataPrefix, + schema => $metadata_format->{schema}, + metadataNamespace => $metadata_format->{metadataNamespace} + ) ); + } + + return $response; +} + + +sub listSets { + + my ($record_class, $requestURL ) = @_; + + if ($oai_sets) { + my $response = HTTP::OAI::ListSets->new( ); + foreach my $key (keys $oai_sets) { + my $set = $oai_sets->{$key} ; + if ( $set && $set->{setSpec} && $set->{record_class} eq $record_class ) { + $response->set( + HTTP::OAI::Set->new( + setSpec => $set->{setSpec}, + setName => $set->{setName} + ) + ); + } + } + return $response; + } else { + my @errors = (new HTTP::OAI::Error(code=>'noSetHierarchy', message=>'The repository does not support sets.') ) ; + my $response = HTTP::OAI::Response->new( requestURL => $requestURL ); + $response->errors(@errors); + return $response; + } +} + + +sub getRecord { + + my ($record_class, $requestURL, $identifier, $metadataPrefix ) = @_; + + my $response ; + my @errors; + + # Do we have a valid identifier ? + my $regex_identifier = "^${scheme}${delimiter}${repository_identifier}${delimiter}([0-9]+)\$"; + if ( $identifier =~ /$regex_identifier/i ) { + my $tcn = $1 ; + + # Do we have a record ? + my $record = $oai->request('open-ils.oai.list.retrieve', $record_class, $tcn, undef, undef, undef, $metadataPrefix, 1, $deleted_record)->gather(1) ; + if (@$record) { + $response = HTTP::OAI::GetRecord->new(); + my $o = "Fieldmapper::oai::$record_class"->new(@$record[0]); + $response->record(_record($record_class, $o, $metadataPrefix)); + } else { + push @errors, new HTTP::OAI::Error(code=>'idDoesNotExist', message=>'The value of the identifier argument is unknown or illegal in this repository.') ; + } + } + else { + push @errors, new HTTP::OAI::Error(code=>'idDoesNotExist', message=>'The value of the identifier argument is unknown or illegal in this repository.') ; + } + + if (@errors) { + $response = HTTP::OAI::Response->new( requestURL => $requestURL ); + $response->errors(@errors); + } + + return $response; +} + + +sub listIdentifiers { + + my ($record_class, $requestURL, $from, $until, $set, $metadataPrefix, $offset ) = @_; + my $response; + + my $r = $oai->request('open-ils.oai.list.retrieve', $record_class, $offset, $from, $until, $set, $metadataPrefix, $max_count, $deleted_record)->gather(1) ; + if (@$r) { + my $cursor = 0 ; + $response = HTTP::OAI::ListIdentifiers->new(); + for my $record (@$r) { + my $o = "Fieldmapper::oai::$record_class"->new($record) ; + if ( $cursor++ == $max_count ) { + my $token = new HTTP::OAI::ResumptionToken( resumptionToken => encode_base64(join( '$', $metadataPrefix, $from, $until, $oai_sets->{$set}->{setSpec}, $o->tcn ), '' ) ) ; + $token->cursor($offset); + $response->resumptionToken($token) ; + } else { + $response->identifier( _header($record_class, $o)) ; + } + } + } else { + my @errors = (new HTTP::OAI::Error(code=>'noRecordsMatch', message=>'The combination of the values of the from, until, set, and metadataPrefix arguments results in an empty list.') ) ; + $response = HTTP::OAI::Response->new( requestURL => $requestURL ); + $response->errors(@errors); + } + + return $response ; +} + + +sub listRecords { + + my ($record_class, $requestURL, $from, $until, $set, $metadataPrefix, $offset ) = @_; + my $response; + + my $r = $oai->request('open-ils.oai.list.retrieve', $record_class, $offset, $from, $until, $set, $metadataPrefix, $max_count, $deleted_record)->gather(1) ; + if (@$r) { + my $cursor = 0 ; + $response = HTTP::OAI::ListRecords->new(); + for my $record (@$r) { + my $o = "Fieldmapper::oai::$record_class"->new($record) ; + if ( $cursor++ == $max_count ) { + my $token = new HTTP::OAI::ResumptionToken( resumptionToken => encode_base64(join( '$', $metadataPrefix, $from, $until, $oai_sets->{$set}->{setSpec}, $o->tcn ), '' ) ) ; + $token->cursor($offset); + $response->resumptionToken($token) ; + } else { + $response->record(_record($record_class, $o, $metadataPrefix)); + } + } + } else { + my @errors = (new HTTP::OAI::Error(code=>'noRecordsMatch', message=>'The combination of the values of the from, until, set, and metadataPrefix arguments results in an empty list.') ) ; + $response = HTTP::OAI::Response->new( requestURL => $requestURL ); + $response->errors(@errors); + } + + return $response ; +} + + +sub _header { + + my ($record_class, $o) = @_; + my @set_spec; + + my $status = 'deleted' if ($o->deleted eq 't'); + my $s = $o->set_spec; # Here we get an array that was parsed as a string like "{1,2,3,4}" + $s =~ s/[{}]//g ; # We remove the {} + foreach (split(',', $s)) { # and turn this into an array. + my $_set = $oai_sets->{$_}; + push @set_spec, $_set->{setSpec} if ( $_set && $_set->{record_class} eq $record_class) ; + } + + return new HTTP::OAI::Header( + identifier => $scheme . $delimiter . $repository_identifier . $delimiter . $o->tcn, + datestamp => substr($o->datestamp, 0, 19) . 'Z', + status => $status, + setSpec => \@set_spec + ) +} + + +sub _record { + + my ($record_class, $o, $metadataPrefix ) = @_; + + my $record = HTTP::OAI::Record->new(); + $record->header( _header($record_class, $o) ); + + if ( $o->deleted eq 'f' ) { + my $md = new HTTP::OAI::Metadata() ; + my $xml = $oai->request('open-ils.oai.' . $record_class . '.retrieve', $o->tcn, $metadataPrefix)->gather(1) ; + $md->dom( $parser->parse_string('' . $xml . '') ); # Not sure why I need to add the metadata element, + $record->metadata( $md ); # because I expect ->metadata() would provide the wrapper for it. + } + + return $record ; +} + + +# _load_oaisets_authority +# Populate the $oai_sets hash with the sets for authority records. +# oai_sets = {id\setSpec => {id, setSpec, setName, record_class = 'authority' }} +sub _load_oaisets_authority { + + my $ses = OpenSRF::AppSession->create('open-ils.cstore'); + my $r = $ses->request('open-ils.cstore.direct.authority.browse_axis.search.atomic', + {code => {'!=' => undef } } )->gather(1); + + for my $record (@$r) { + my $o = Fieldmapper::authority::browse_axis->new($record) ; + $oai_sets->{$o->code} = { + id => $o->code, + setSpec => $o->code, + setName => $o->description, # description is more verbose than $o->name + record_class => 'authority' + }; + } +} + + +# _load_oaisets_biblio +# Populate the $oai_sets hash with the sets for bibliographic records. Those are org_type records +# oai_sets = {id\setSpec => {id, setSpec, setName, record_class = 'biblio' }} +sub _load_oaisets_biblio { + + my $node = shift; + my $types = shift; + my $parent = shift; + + unless ( $node ) { + my $ses = OpenSRF::AppSession->create('open-ils.actor'); + $node = $ses->request('open-ils.actor.org_tree.retrieve')->gather(1); + my $aout = $ses->request('open-ils.actor.org_types.retrieve')->gather(1); + $ses->disconnect; + return unless ($node) ; + + my @_types; + foreach my $type (@$aout) { + $_types[int($type->id)] = $type; + } + $types = \@_types; + } + + return unless ($node->opac_visible =~ /^[y1t]+/i); + + my $spec = ($parent) ? $parent . ':' . $node->shortname : $node->shortname ; + $oai_sets->{$spec} = {id => $node->id, record_class => 'biblio' }; + $oai_sets->{$node->id} = {setSpec => $spec, setName => $node->name, record_class => 'biblio' }; + + my $kids = $node->children; + _load_oaisets_biblio($_, $types, $spec) for (@$kids); +} + + +# _load_oai_metadataformats +# Populate the $oai_metadataformats hash with the supported metadata formats: +# oai_metadataformats = { metadataPrefix => { schema, metadataNamespace } } +sub _load_oai_metadataformats { + + my $list = $oai->request('open-ils.oai.record.formats')->gather(1); + for my $record_browse_format ( @$list ) { + my %h = %$record_browse_format ; + my $metadataPrefix = (keys %h)[0] ; + $oai_metadataformats->{$metadataPrefix} = { + schema => $h{$metadataPrefix}->{'namespace_uri'}, + metadataNamespace => $h{$metadataPrefix}->{'schema_location'} + }; + } +} + +1; diff --git a/Open-ILS/src/sql/Pg/oai.sql b/Open-ILS/src/sql/Pg/oai.sql new file mode 100644 index 0000000000..64688b9dac --- /dev/null +++ b/Open-ILS/src/sql/Pg/oai.sql @@ -0,0 +1,68 @@ +-- VIEWS for the oai service +CREATE SCHEMA oai; + +-- The view presents a lean table with unique bre.tc-numbers for oai paging; +CREATE VIEW oai.biblio AS + SELECT + bre.id AS tcn, + bre.edit_date AS datestamp, + bre.deleted AS deleted + FROM + biblio.record_entry bre + ORDER BY + bre.id; + +-- The view presents a lean table with unique are.tc-numbers for oai paging; +CREATE VIEW oai.authority AS + SELECT + are.id AS tcn, + are.edit_date AS datestamp, + are.deleted AS deleted + FROM + authority.record_entry AS are + ORDER BY + are.id; + +-- If an edit date changes in the asset.call_number or asset.copy and you want this to persist to an OAI2 datestamp, +-- then add these stored procedures and triggers: +CREATE OR REPLACE FUNCTION oai.datestamp(rid BIGINT) + RETURNS VOID AS $$ +BEGIN + UPDATE biblio.record_entry AS bre + SET edit_date = now() + WHERE bre.id = rid; +END +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION oai.call_number_datestamp() + RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'DELETE' + THEN + PERFORM oai.datestamp(OLD.record); + RETURN OLD; + END IF; + + PERFORM oai.datestamp(NEW.record); + RETURN NEW; + +END +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION oai.copy_datestamp() + RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'DELETE' + THEN + PERFORM oai.datestamp((SELECT acn.record FROM asset.call_number as acn WHERE acn.id = OLD.call_number)); + RETURN OLD; + END IF; + + PERFORM oai.datestamp((SELECT acn.record FROM asset.call_number as acn WHERE acn.id = NEW.call_number)); + RETURN NEW; + +END +$$ LANGUAGE plpgsql; + +CREATE TRIGGER call_number_datestamp AFTER INSERT OR UPDATE OR DELETE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE oai.call_number_datestamp(); +CREATE TRIGGER copy_datestamp AFTER INSERT OR UPDATE OR DELETE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE oai.copy_datestamp(); \ No newline at end of file diff --git a/Open-ILS/xsl/OAI2_MARC21slim.xsl b/Open-ILS/xsl/OAI2_MARC21slim.xsl new file mode 100644 index 0000000000..3bf4226953 --- /dev/null +++ b/Open-ILS/xsl/OAI2_MARC21slim.xsl @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Open-ILS/xsl/OAI2_OAIDC.xsl b/Open-ILS/xsl/OAI2_OAIDC.xsl new file mode 100644 index 0000000000..4434360f20 --- /dev/null +++ b/Open-ILS/xsl/OAI2_OAIDC.xsl @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/RELEASE_NOTES_NEXT/OAI2/install.adoc b/docs/RELEASE_NOTES_NEXT/OAI2/install.adoc new file mode 100644 index 0000000000..e54eb3a512 --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/OAI2/install.adoc @@ -0,0 +1,408 @@ += oai-openils is an openSRF service + +This module is an optional service that exposes your catalog through the [OAI2 protocol](http://www.openarchives.org/OAI/openarchivesprotocol.html). + +== 1. Intended behaviour + +=== 1.1 Entry points +There are two: one for bibliographic records and one for authority records: + + http://your-domain/opac/extras/oai/authority + http://your-domain/opac/extras/oai/biblio + +=== 1.2 Setspec are not implemented + +This is a work in progress and not enabled. The aim is to have the owning library determine the set hierarchy. The Concerto +test database for example has a record with tcn #1. This record is so popular it has copies attached to library units +"Example Branch 1", "Example Branch 2", "Example Branch 3", "Example Bookmobile 1" which is a child of Branch 3 and +"Example Branch 4". This entire kinship is expressed as sets like so: + +```xml +
+ ... + CONS + CONS:SYS1 + CONS:SYS2 + CONS:SYS1:BR1 + CONS:SYS1:BR2 + CONS:SYS2:BR3 + CONS:SYS2:BR4 + CONS:SYS2:BR3:BM1 +
+``` +Likewise the setSpecs of authority records are derived from their browse axis ( Title, Author, Subject and Topic ). + +=== 1.3 OAI2 datestamp + +The edit date of the bibliographic and authority record is used as datestamp. If you want the date for editorial updates +of bibliographic assets ( copies, call numbers ) reflected in the datestamp, then add the triggers shown below. + +=== 1.4 Bibliographic mapping of assets to 852 subfields + +Certain attributes asset are placed into 852 subfields so: + +| subfield code | asset resource | +| --- | --- | +| a | location | +| b | owning_lib | +| c | callnumber | +| d | circlib | +| g | barcode | +| n | status | + +Thus the Concerto with tcn #1 will have it's 852 subfields expressed as: +```xml + + Stacks + BR4 + ML 60 R100 + BR4 + CONC70000435 + Checked out + +``` +This mapping can be customized and extended with static subfields: +```xml + A constant value +``` + +=== 1.5 Default configuration + +All default configuration is commented in the open-ils.oai app_settings element. See below for details on how to +override defaults by removing the comments and substitute the values. + +== 2. Installation + +=== 2.1 Perl modules + +Lookup the Perl handler and the associated openils module: + + - [Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm](Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm) + - [Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm](Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm) + +Place them in your codebase next to the other openils modules and let them thus become part of the build: + + Open-ILS/src/perlmods/lib/OpenILS/Application/OAI.pm + Open-ILS/src/perlmods/lib/OpenILS/WWW/OAI.pm + +or copy the files (owned by the opensrf user) on your servers that host the openils services in the Perl library path: + + /the perl library path/OpenILS/Application/OAI.pm + /the perl library path/OpenILS/WWW/OAI.pm + +=== 2.2 Declare the perl handler + +Declare the Perl handler in the Apache eg_startup file: + +```perl +use OpenILS::WWW::OAI qw( conf/opensrf_core.xml ); +``` + +And reference it in the Apache eg_vhost.conf file, apache 2.2: + + + SetHandler perl-script + PerlHandler OpenILS::WWW::OAI + Options +ExecCGI + PerlSendHeader On + allow from all + + +or apache 2.4 + + + SetHandler perl-script + PerlHandler OpenILS::WWW::OAI + Options +ExecCGI + PerlSendHeader On + Require all granted + + +In the eg.conf file under 'PerlRequire /etc/apache2/eg_startup' add: +```apache +PerlChildInitHandler OpenILS::WWW::OAI::child_init + +``` + +=== 2.3 The database and fieldmapper + +==== 2.3.1 Database + +The service requires a view and stored procedures: Open-ILS/src/sql/Pg/oai.sql + +Add the oai section to the database: +```sql +-- VIEWS for the oai service +CREATE SCHEMA oai; + + +-- The view presents a lean table with unique bre.tc-numbers for oai paging; +CREATE VIEW oai.biblio AS + SELECT + bre.id AS tcn, + bre.edit_date AS datestamp, + bre.deleted AS deleted + FROM + biblio.record_entry bre + ORDER BY + bre.id; + +-- The view presents a lean table with unique are.tc-numbers for oai paging; +CREATE VIEW oai.authority AS + SELECT + are.id AS tcn, + are.edit_date AS datestamp, + are.deleted AS deleted + FROM + authority.record_entry AS are + ORDER BY + are.id; +``` + +==== 2.3.2 Optional, setting the datestamp + +If you want the OAI2 datestamp to reflect changes in assets as well, add the following triggers + ```sql + +-- If an edit date changes in the asset.call_number or asset.copy and you want this to persist to an OAI2 datestamp, +-- then add these stored procedures and triggers: +CREATE OR REPLACE FUNCTION oai.datestamp(rid BIGINT) + RETURNS VOID AS $$ +BEGIN + UPDATE biblio.record_entry AS bre + SET edit_date = now() + WHERE bre.id = rid; +END +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION oai.call_number_datestamp() + RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'DELETE' + THEN + PERFORM oai.datestamp(OLD.record); + RETURN OLD; + END IF; + + PERFORM oai.datestamp(NEW.record); + RETURN NEW; + +END +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION oai.copy_datestamp() + RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'DELETE' + THEN + PERFORM oai.datestamp((SELECT acn.record FROM asset.call_number as acn WHERE acn.id = OLD.call_number)); + RETURN OLD; + END IF; + + PERFORM oai.datestamp((SELECT acn.record FROM asset.call_number as acn WHERE acn.id = NEW.call_number)); + RETURN NEW; + +END +$$ LANGUAGE plpgsql; + +CREATE TRIGGER call_number_datestamp AFTER INSERT OR UPDATE OR DELETE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE oai.call_number_datestamp(); +CREATE TRIGGER copy_datestamp AFTER INSERT OR UPDATE OR DELETE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE oai.copy_datestamp(); + ``` + +==== 2.3.3 The fieldmapper + +Proceed by declaring the views in the fm_IDL.xml file so, as the example shows here [Open-ILS/examples/fm_ILD.xml](Open-ILS/examples/fm_IDL.xml): + +```xml + + + + + + + + + + + + + + + + +``` + +=== 2.4 The xslt stylesheets + +Lookup the two documents here: + + - [Open-ILS/xsl/OAI2_OAIDC.xsl](Open-ILS/xsl/OAI2_OAIDC.xsl) + - [Open-ILS/xsl/OAI2_MARC21slim.xsl](Open-ILS/xsl/OAI2_MARC21slim.xsl) + +Place the stylesheets in your codebase next to the other xsl documents and let them thus become part of the build. +Or install them on your servers that host the openils services: + + //var/xsl/OAI2_OAIDC.xsl + //var/xsl/OAI2_MARC21slim.xsl + +=== 2.5 Dependencies +The openils-oai service depends on a running openils-supercat service. +And the OAI2_OAIDC.xsl document uses the file [MARC21slim2OAIDC.xsl](Open-ILS/xsl/MARC21slim2OAIDC.xsl). +The service and stylesheet are part of the out-of-the-box Evergreen distributions. + +But do install the ['HTTP::OAI' perl library from a CPAN repository](http://search.cpan.org/dist/HTTP-OAI/): + + $ cpan HTTP::OAI + + +== 3. Configuration + +=== 3.1 Declare the service + +Add the openils-oai service to your /<openils sysdir>/conf/opensrf.xml file. +```xml +.... + + 5 + 1 + perl + OpenILS::Application::OAI + 199 + + open-ils.oai_unix.sock + open-ils.oai_unix.pid + 1000 + open-ils.oai_unix.log + 1 + 5 + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +==== 3.2 Activate the service + +Refer to the service in the opensrf.xml's activeapps element: +```xml +.... + + open-ils.oai +``` + +==== 3.3 Register the service with the router + +Add the service to the public router with your /<openils sysdir>/conf/opensrf_core.xml +```xml + + + + + router + public.realm + + openils.oai + ... +``` + + + + + + -- 2.11.0