de0161db1e
Currently, the rules used to create OAI sets are processed with the 'or' boolean operator between each rule. This patch allows to use 'or' or 'and' between the rules. The evaluation of the rules is done according to the boolean operators precedence: AND has a higher precedence than OR. For example: A and B or C and D will be evaluated as follow: (A and B) or (C and D) Test plan: - Apply the patch - Apply the atomicupdate - Prove t/db_dependant/OAI/AndSets.t - Check that existing mappings still work - Try modifying existing mappings - Try creating new mappings - Check that the boolean operators precedence is correctly taken into account Signed-off-by: Michal Denar <black23@gmail.com> Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
642 lines
16 KiB
Perl
642 lines
16 KiB
Perl
package C4::OAI::Sets;
|
|
|
|
# Copyright 2011 BibLibre
|
|
#
|
|
# This file is part of Koha.
|
|
#
|
|
# Koha 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.
|
|
#
|
|
# Koha 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 Koha; if not, see <http://www.gnu.org/licenses>.
|
|
|
|
=head1 NAME
|
|
|
|
C4::OAI::Sets - OAI Sets management functions
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
C4::OAI::Sets contains functions for managing storage and editing of OAI Sets.
|
|
|
|
OAI Set description can be found L<here|http://www.openarchives.org/OAI/openarchivesprotocol.html#Set>
|
|
|
|
=cut
|
|
|
|
use Modern::Perl;
|
|
use C4::Context;
|
|
|
|
use vars qw(@ISA @EXPORT);
|
|
|
|
BEGIN {
|
|
require Exporter;
|
|
@ISA = qw(Exporter);
|
|
@EXPORT = qw(
|
|
&GetOAISets &GetOAISet &GetOAISetBySpec &ModOAISet &DelOAISet &AddOAISet
|
|
&GetOAISetsMappings &GetOAISetMappings &ModOAISetMappings
|
|
&GetOAISetsBiblio &ModOAISetsBiblios &AddOAISetsBiblios
|
|
&CalcOAISetsBiblio &UpdateOAISetsBiblio &DelOAISetsBiblio
|
|
);
|
|
}
|
|
|
|
=head1 FUNCTIONS
|
|
|
|
=head2 GetOAISets
|
|
|
|
$oai_sets = GetOAISets;
|
|
|
|
GetOAISets return a array reference of hash references describing the sets.
|
|
The hash references looks like this:
|
|
|
|
{
|
|
'name' => 'set name',
|
|
'spec' => 'set spec',
|
|
'descriptions' => [
|
|
'description 1',
|
|
'description 2',
|
|
...
|
|
]
|
|
}
|
|
|
|
=cut
|
|
|
|
sub GetOAISets {
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
SELECT * FROM oai_sets
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute;
|
|
my $results = $sth->fetchall_arrayref({});
|
|
|
|
$query = qq{
|
|
SELECT description
|
|
FROM oai_sets_descriptions
|
|
WHERE set_id = ?
|
|
};
|
|
$sth = $dbh->prepare($query);
|
|
foreach my $set (@$results) {
|
|
$sth->execute($set->{'id'});
|
|
my $desc = $sth->fetchall_arrayref({});
|
|
foreach (@$desc) {
|
|
push @{$set->{'descriptions'}}, $_->{'description'};
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
=head2 GetOAISet
|
|
|
|
$set = GetOAISet($set_id);
|
|
|
|
GetOAISet returns a hash reference describing the set with the given set_id.
|
|
|
|
See GetOAISets to see what the hash looks like.
|
|
|
|
=cut
|
|
|
|
sub GetOAISet {
|
|
my ($set_id) = @_;
|
|
|
|
return unless $set_id;
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
SELECT *
|
|
FROM oai_sets
|
|
WHERE id = ?
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute($set_id);
|
|
my $set = $sth->fetchrow_hashref;
|
|
|
|
$query = qq{
|
|
SELECT description
|
|
FROM oai_sets_descriptions
|
|
WHERE set_id = ?
|
|
};
|
|
$sth = $dbh->prepare($query);
|
|
$sth->execute($set->{'id'});
|
|
my $desc = $sth->fetchall_arrayref({});
|
|
foreach (@$desc) {
|
|
push @{$set->{'descriptions'}}, $_->{'description'};
|
|
}
|
|
|
|
return $set;
|
|
}
|
|
|
|
=head2 GetOAISetBySpec
|
|
|
|
my $set = GetOAISetBySpec($setSpec);
|
|
|
|
Returns a hash describing the set whose spec is $setSpec
|
|
|
|
=cut
|
|
|
|
sub GetOAISetBySpec {
|
|
my $setSpec = shift;
|
|
|
|
return unless defined $setSpec;
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
SELECT *
|
|
FROM oai_sets
|
|
WHERE spec = ?
|
|
LIMIT 1
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute($setSpec);
|
|
|
|
return $sth->fetchrow_hashref;
|
|
}
|
|
|
|
=head2 ModOAISet
|
|
|
|
my $set = {
|
|
'id' => $set_id, # mandatory
|
|
'spec' => $spec, # mandatory
|
|
'name' => $name, # mandatory
|
|
'descriptions => \@descriptions, # optional, [] to remove descriptions
|
|
};
|
|
ModOAISet($set);
|
|
|
|
ModOAISet modify a set in the database.
|
|
|
|
=cut
|
|
|
|
sub ModOAISet {
|
|
my ($set) = @_;
|
|
|
|
return unless($set && $set->{'spec'} && $set->{'name'});
|
|
|
|
if(!defined $set->{'id'}) {
|
|
warn "Set ID not defined, can't modify the set";
|
|
return;
|
|
}
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
UPDATE oai_sets
|
|
SET spec = ?,
|
|
name = ?
|
|
WHERE id = ?
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute($set->{'spec'}, $set->{'name'}, $set->{'id'});
|
|
|
|
if($set->{'descriptions'}) {
|
|
$query = qq{
|
|
DELETE FROM oai_sets_descriptions
|
|
WHERE set_id = ?
|
|
};
|
|
$sth = $dbh->prepare($query);
|
|
$sth->execute($set->{'id'});
|
|
|
|
if(scalar @{$set->{'descriptions'}} > 0) {
|
|
$query = qq{
|
|
INSERT INTO oai_sets_descriptions (set_id, description)
|
|
VALUES (?,?)
|
|
};
|
|
$sth = $dbh->prepare($query);
|
|
foreach (@{ $set->{'descriptions'} }) {
|
|
$sth->execute($set->{'id'}, $_) if $_;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
=head2 DelOAISet
|
|
|
|
DelOAISet($set_id);
|
|
|
|
DelOAISet remove the set with the given set_id
|
|
|
|
=cut
|
|
|
|
sub DelOAISet {
|
|
my ($set_id) = @_;
|
|
|
|
return unless $set_id;
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
DELETE oai_sets, oai_sets_descriptions, oai_sets_mappings
|
|
FROM oai_sets
|
|
LEFT JOIN oai_sets_descriptions ON oai_sets_descriptions.set_id = oai_sets.id
|
|
LEFT JOIN oai_sets_mappings ON oai_sets_mappings.set_id = oai_sets.id
|
|
WHERE oai_sets.id = ?
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute($set_id);
|
|
}
|
|
|
|
=head2 AddOAISet
|
|
|
|
my $set = {
|
|
'id' => $set_id, # mandatory
|
|
'spec' => $spec, # mandatory
|
|
'name' => $name, # mandatory
|
|
'descriptions => \@descriptions, # optional
|
|
};
|
|
my $set_id = AddOAISet($set);
|
|
|
|
AddOAISet adds a new set and returns its id, or undef if something went wrong.
|
|
|
|
=cut
|
|
|
|
sub AddOAISet {
|
|
my ($set) = @_;
|
|
|
|
return unless($set && $set->{'spec'} && $set->{'name'});
|
|
|
|
my $set_id;
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
INSERT INTO oai_sets (spec, name)
|
|
VALUES (?,?)
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
if( $sth->execute($set->{'spec'}, $set->{'name'}) ) {
|
|
$set_id = $dbh->last_insert_id(undef, undef, 'oai_sets', undef);
|
|
if($set->{'descriptions'}) {
|
|
$query = qq{
|
|
INSERT INTO oai_sets_descriptions (set_id, description)
|
|
VALUES (?,?)
|
|
};
|
|
$sth = $dbh->prepare($query);
|
|
foreach( @{ $set->{'descriptions'} } ) {
|
|
$sth->execute($set_id, $_) if $_;
|
|
}
|
|
}
|
|
} else {
|
|
warn "AddOAISet failed";
|
|
}
|
|
|
|
return $set_id;
|
|
}
|
|
|
|
=head2 GetOAISetsMappings
|
|
|
|
my $mappings = GetOAISetsMappings;
|
|
|
|
GetOAISetsMappings returns mappings for all OAI Sets.
|
|
|
|
Mappings define how biblios are categorized in sets.
|
|
A mapping is defined by six properties:
|
|
|
|
{
|
|
marcfield => 'XXX', # the MARC field to check
|
|
marcsubfield => 'Y', # the MARC subfield to check
|
|
operator => 'A', # the operator 'equal' or 'notequal'; 'equal' if ''
|
|
marcvalue => 'zzzz', # the value to check
|
|
rule_operator => 'and|or|undef', # the operator between the rules
|
|
rule_order => 'n' # the order of the rule for the mapping
|
|
}
|
|
|
|
If defined in a set mapping, a biblio which have at least one 'Y' subfield of
|
|
one 'XXX' field equal to 'zzzz' will belong to this set.
|
|
|
|
GetOAISetsMappings returns a hashref of arrayrefs of hashrefs.
|
|
The first hashref keys are the sets IDs, so it looks like this:
|
|
|
|
$mappings = {
|
|
'1' => [
|
|
{
|
|
marcfield => 'XXX',
|
|
marcsubfield => 'Y',
|
|
operator => 'A',
|
|
marcvalue => 'zzzz',
|
|
rule_operator => 'and|or|undef',
|
|
rule_order => 'n'
|
|
},
|
|
{
|
|
...
|
|
},
|
|
...
|
|
],
|
|
'2' => [...],
|
|
...
|
|
};
|
|
|
|
=cut
|
|
|
|
sub GetOAISetsMappings {
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
SELECT * FROM oai_sets_mappings ORDER BY set_id, rule_order
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute;
|
|
|
|
my $mappings = {};
|
|
while(my $result = $sth->fetchrow_hashref) {
|
|
push @{ $mappings->{$result->{'set_id'}} }, {
|
|
marcfield => $result->{'marcfield'},
|
|
marcsubfield => $result->{'marcsubfield'},
|
|
operator => $result->{'operator'},
|
|
marcvalue => $result->{'marcvalue'},
|
|
rule_operator => $result->{'rule_operator'},
|
|
rule_order => $result->{'rule_order'}
|
|
};
|
|
}
|
|
|
|
return $mappings;
|
|
}
|
|
|
|
=head2 GetOAISetMappings
|
|
|
|
my $set_mappings = GetOAISetMappings($set_id);
|
|
|
|
Return mappings for the set with given set_id. It's an arrayref of hashrefs
|
|
|
|
=cut
|
|
|
|
sub GetOAISetMappings {
|
|
my ($set_id) = @_;
|
|
|
|
return unless $set_id;
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
SELECT *
|
|
FROM oai_sets_mappings
|
|
WHERE set_id = ?
|
|
ORDER BY rule_order
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute($set_id);
|
|
|
|
my @mappings;
|
|
while(my $result = $sth->fetchrow_hashref) {
|
|
push @mappings, {
|
|
marcfield => $result->{'marcfield'},
|
|
marcsubfield => $result->{'marcsubfield'},
|
|
operator => $result->{'operator'},
|
|
marcvalue => $result->{'marcvalue'},
|
|
rule_operator => $result->{'rule_operator'},
|
|
rule_order => $result->{'rule_order'}
|
|
};
|
|
}
|
|
|
|
return \@mappings;
|
|
}
|
|
|
|
=head2 ModOAISetMappings {
|
|
|
|
my $mappings = [
|
|
{
|
|
marcfield => 'XXX',
|
|
marcsubfield => 'Y',
|
|
operator => 'A',
|
|
marcvalue => 'zzzz'
|
|
},
|
|
...
|
|
];
|
|
ModOAISetMappings($set_id, $mappings);
|
|
|
|
ModOAISetMappings modifies mappings of a given set.
|
|
|
|
=cut
|
|
|
|
sub ModOAISetMappings {
|
|
my ($set_id, $mappings) = @_;
|
|
|
|
return unless $set_id;
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
DELETE FROM oai_sets_mappings
|
|
WHERE set_id = ?
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute($set_id);
|
|
if(scalar @$mappings > 0) {
|
|
$query = qq{
|
|
INSERT INTO oai_sets_mappings (set_id, marcfield, marcsubfield, operator, marcvalue, rule_operator, rule_order)
|
|
VALUES (?,?,?,?,?,?,?)
|
|
};
|
|
$sth = $dbh->prepare($query);
|
|
foreach (@$mappings) {
|
|
$sth->execute($set_id, $_->{'marcfield'}, $_->{'marcsubfield'}, $_->{'operator'}, $_->{'marcvalue'}, $_->{'rule_operator'}, $_->{'rule_order'});
|
|
}
|
|
}
|
|
}
|
|
|
|
=head2 GetOAISetsBiblio
|
|
|
|
$oai_sets = GetOAISetsBiblio($biblionumber);
|
|
|
|
Return the OAI sets where biblio appears.
|
|
|
|
Return value is an arrayref of hashref where each element of the array is a set.
|
|
Keys of hash are id, spec and name
|
|
|
|
=cut
|
|
|
|
sub GetOAISetsBiblio {
|
|
my ($biblionumber) = @_;
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
SELECT oai_sets.*
|
|
FROM oai_sets
|
|
LEFT JOIN oai_sets_biblios ON oai_sets_biblios.set_id = oai_sets.id
|
|
WHERE biblionumber = ?
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
|
|
$sth->execute($biblionumber);
|
|
return $sth->fetchall_arrayref({});
|
|
}
|
|
|
|
=head2 DelOAISetsBiblio
|
|
|
|
DelOAISetsBiblio($biblionumber);
|
|
|
|
Remove a biblio from all sets
|
|
|
|
=cut
|
|
|
|
sub DelOAISetsBiblio {
|
|
my ($biblionumber) = @_;
|
|
|
|
return unless $biblionumber;
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
DELETE FROM oai_sets_biblios
|
|
WHERE biblionumber = ?
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
return $sth->execute($biblionumber);
|
|
}
|
|
|
|
=head2 CalcOAISetsBiblio
|
|
|
|
my @sets = CalcOAISetsBiblio($record, $oai_sets_mappings);
|
|
|
|
Return a list of set ids the record belongs to. $record must be a MARC::Record
|
|
and $oai_sets_mappings (optional) must be a hashref returned by
|
|
GetOAISetsMappings
|
|
|
|
=cut
|
|
|
|
sub CalcOAISetsBiblio {
|
|
my ($record, $oai_sets_mappings) = @_;
|
|
|
|
return unless $record;
|
|
|
|
$oai_sets_mappings ||= GetOAISetsMappings;
|
|
|
|
my @biblio_sets;
|
|
foreach my $set_id (keys %$oai_sets_mappings) {
|
|
|
|
my $rules = [];
|
|
foreach my $mapping (@{ $oai_sets_mappings->{$set_id} }) {
|
|
next if not $mapping;
|
|
my $rule_operator = $mapping->{'rule_operator'};
|
|
my $result = _evalRule($record, $mapping);
|
|
|
|
# First rule or 'or' rule is always pushed
|
|
if (!@$rules || $rule_operator eq 'or') {
|
|
push @$rules, [$result];
|
|
next;
|
|
}
|
|
|
|
# 'and' rule is pushed in the last 'or' rule
|
|
push @{$rules->[-1]}, $result;
|
|
}
|
|
|
|
my @evaluated_and;
|
|
foreach my $ruleset (@$rules) {
|
|
if (0 < grep /0/, @{$ruleset}) {
|
|
push @evaluated_and, 0;
|
|
} else {
|
|
push @evaluated_and, 1;
|
|
}
|
|
}
|
|
|
|
if (grep /1/, @evaluated_and) {
|
|
push @biblio_sets, $set_id;
|
|
}
|
|
|
|
}
|
|
return @biblio_sets;
|
|
}
|
|
|
|
# Does the record match a given mapping rule?
|
|
sub _evalRule {
|
|
my $record = shift;
|
|
my $mapping = shift;
|
|
|
|
my $field = $mapping->{'marcfield'};
|
|
my $subfield = $mapping->{'marcsubfield'};
|
|
my $operator = $mapping->{'operator'};
|
|
my $value = $mapping->{'marcvalue'};
|
|
my @subfield_values = $record->subfield($field, $subfield);
|
|
if ($operator eq 'notequal') {
|
|
if(0 == grep /^$value$/, @subfield_values) {
|
|
return 1;
|
|
}
|
|
}
|
|
else {
|
|
if(0 < grep /^$value$/, @subfield_values) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
=head2 ModOAISetsBiblios
|
|
|
|
my $oai_sets_biblios = {
|
|
'1' => [1, 3, 4], # key is the set_id, and value is an array ref of biblionumbers
|
|
'2' => [],
|
|
...
|
|
};
|
|
ModOAISetsBiblios($oai_sets_biblios);
|
|
|
|
ModOAISetsBiblios deletes all records from oai_sets_biblios table and calls AddOAISetsBiblios.
|
|
This table is then used in opac/oai.pl.
|
|
|
|
=cut
|
|
|
|
sub ModOAISetsBiblios {
|
|
my $oai_sets_biblios = shift;
|
|
|
|
return unless ref($oai_sets_biblios) eq "HASH";
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
DELETE FROM oai_sets_biblios
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
$sth->execute;
|
|
AddOAISetsBiblios($oai_sets_biblios);
|
|
}
|
|
|
|
=head2 UpdateOAISetsBiblio
|
|
|
|
UpdateOAISetsBiblio($biblionumber, $record);
|
|
|
|
Update OAI sets for one biblio. The two parameters are mandatory.
|
|
$record is a MARC::Record.
|
|
|
|
=cut
|
|
|
|
sub UpdateOAISetsBiblio {
|
|
my ($biblionumber, $record) = @_;
|
|
|
|
return unless($biblionumber and $record);
|
|
|
|
my $sets_biblios;
|
|
my @sets = CalcOAISetsBiblio($record);
|
|
foreach (@sets) {
|
|
push @{ $sets_biblios->{$_} }, $biblionumber;
|
|
}
|
|
DelOAISetsBiblio($biblionumber);
|
|
AddOAISetsBiblios($sets_biblios);
|
|
}
|
|
|
|
=head2 AddOAISetsBiblios
|
|
|
|
my $oai_sets_biblios = {
|
|
'1' => [1, 3, 4], # key is the set_id, and value is an array ref of biblionumbers
|
|
'2' => [],
|
|
...
|
|
};
|
|
ModOAISetsBiblios($oai_sets_biblios);
|
|
|
|
AddOAISetsBiblios insert given infos in oai_sets_biblios table.
|
|
This table is then used in opac/oai.pl.
|
|
|
|
=cut
|
|
|
|
sub AddOAISetsBiblios {
|
|
my $oai_sets_biblios = shift;
|
|
|
|
return unless ref($oai_sets_biblios) eq "HASH";
|
|
|
|
my $dbh = C4::Context->dbh;
|
|
my $query = qq{
|
|
INSERT INTO oai_sets_biblios (set_id, biblionumber)
|
|
VALUES (?,?)
|
|
};
|
|
my $sth = $dbh->prepare($query);
|
|
foreach my $set_id (keys %$oai_sets_biblios) {
|
|
foreach my $biblionumber (@{$oai_sets_biblios->{$set_id}}) {
|
|
$sth->execute($set_id, $biblionumber);
|
|
}
|
|
}
|
|
}
|
|
|
|
1;
|