Lines 42-49
use YAML::XS;
Link Here
|
42 |
|
42 |
|
43 |
use List::Util qw( sum0 ); |
43 |
use List::Util qw( sum0 ); |
44 |
use MARC::File::XML; |
44 |
use MARC::File::XML; |
45 |
use MIME::Base64 qw( encode_base64 ); |
45 |
use MIME::Base64 qw(encode_base64 decode_base64); |
46 |
use Encode qw( encode ); |
46 |
use Encode qw(encode decode); |
47 |
use Business::ISBN; |
47 |
use Business::ISBN; |
48 |
use Scalar::Util qw( looks_like_number ); |
48 |
use Scalar::Util qw( looks_like_number ); |
49 |
|
49 |
|
Lines 548-554
sub marc_records_to_documents {
Link Here
|
548 |
my $control_fields_rules = $rules->{control_fields}; |
548 |
my $control_fields_rules = $rules->{control_fields}; |
549 |
my $data_fields_rules = $rules->{data_fields}; |
549 |
my $data_fields_rules = $rules->{data_fields}; |
550 |
my $marcflavour = lc C4::Context->preference('marcflavour'); |
550 |
my $marcflavour = lc C4::Context->preference('marcflavour'); |
551 |
my $use_array = C4::Context->preference('ElasticsearchMARCFormat') eq 'ARRAY'; |
|
|
552 |
|
551 |
|
553 |
my @record_documents; |
552 |
my @record_documents; |
554 |
|
553 |
|
Lines 748-782
sub marc_records_to_documents {
Link Here
|
748 |
} |
747 |
} |
749 |
} |
748 |
} |
750 |
} |
749 |
} |
|
|
750 |
my $preferred_format = C4::Context->preference('ElasticsearchMARCFormat'); |
751 |
|
751 |
|
752 |
# TODO: Perhaps should check if $records_document non empty, but really should never be the case |
752 |
my ($encoded_record, $format) = $self->search_document_marc_record_encode( |
753 |
$record->encoding('UTF-8'); |
753 |
$record, |
754 |
if ($use_array) { |
754 |
$preferred_format, |
755 |
$record_document->{'marc_data_array'} = $self->_marc_to_array($record); |
755 |
$marcflavour |
756 |
$record_document->{'marc_format'} = 'ARRAY'; |
756 |
); |
|
|
757 |
|
758 |
if ($preferred_format eq 'ARRAY') { |
759 |
$record_document->{'marc_data_array'} = $encoded_record; |
757 |
} else { |
760 |
} else { |
758 |
my @warnings; |
761 |
$record_document->{'marc_data'} = $encoded_record; |
759 |
{ |
|
|
760 |
# Temporarily intercept all warn signals (MARC::Record carps when record length > 99999) |
761 |
local $SIG{__WARN__} = sub { |
762 |
push @warnings, $_[0]; |
763 |
}; |
764 |
$record_document->{'marc_data'} = encode_base64(encode('UTF-8', $record->as_usmarc())); |
765 |
} |
766 |
if (@warnings) { |
767 |
# Suppress warnings if record length exceeded |
768 |
unless (substr($record->leader(), 0, 5) eq '99999') { |
769 |
foreach my $warning (@warnings) { |
770 |
carp $warning; |
771 |
} |
772 |
} |
773 |
$record_document->{'marc_data'} = $record->as_xml_record($marcflavour); |
774 |
$record_document->{'marc_format'} = 'MARCXML'; |
775 |
} |
776 |
else { |
777 |
$record_document->{'marc_format'} = 'base64ISO2709'; |
778 |
} |
779 |
} |
762 |
} |
|
|
763 |
$record_document->{'marc_format'} = $format; |
764 |
|
780 |
|
765 |
|
781 |
# Check if there is at least one available item |
766 |
# Check if there is at least one available item |
782 |
if ($self->index eq $BIBLIOS_INDEX) { |
767 |
if ($self->index eq $BIBLIOS_INDEX) { |
Lines 789-795
sub marc_records_to_documents {
Link Here
|
789 |
onloan => undef, |
774 |
onloan => undef, |
790 |
itemlost => 0, |
775 |
itemlost => 0, |
791 |
})->count; |
776 |
})->count; |
792 |
|
|
|
793 |
$record_document->{available} = $avail_items ? \1 : \0; |
777 |
$record_document->{available} = $avail_items ? \1 : \0; |
794 |
} |
778 |
} |
795 |
} |
779 |
} |
Lines 799-804
sub marc_records_to_documents {
Link Here
|
799 |
return \@record_documents; |
783 |
return \@record_documents; |
800 |
} |
784 |
} |
801 |
|
785 |
|
|
|
786 |
=head2 search_document_marc_record_encode($record, $format, $marcflavour) |
787 |
my ($encoded_record, $format) = search_document_marc_record_encode($record, $format, $marcflavour) |
788 |
|
789 |
Encode a MARC::Record to the preferred marc document record format. If record |
790 |
exceeds ISO2709 maximum size record size and C<$format> is set to |
791 |
'base64ISO2709' format will fallback to 'MARCXML' instead. |
792 |
|
793 |
=over 4 |
794 |
|
795 |
=item C<$record> |
796 |
|
797 |
A MARC::Record object |
798 |
|
799 |
=item C<$marcflavour> |
800 |
|
801 |
The marcflavour to use |
802 |
|
803 |
=back |
804 |
|
805 |
=cut |
806 |
|
807 |
sub search_document_marc_record_encode { |
808 |
my ($self, $record, $format, $marcflavour) = @_; |
809 |
|
810 |
$record->encoding('UTF-8'); |
811 |
|
812 |
if ($format eq 'ARRAY') { |
813 |
return ($self->_marc_to_array($record), $format); |
814 |
} |
815 |
elsif ($format eq 'base64ISO2709' || $format eq 'ISO2709') { |
816 |
my @warnings; |
817 |
my $marc_data; |
818 |
{ |
819 |
# Temporarily intercept all warn signals (MARC::Record carps when record length > 99999) |
820 |
local $SIG{__WARN__} = sub { |
821 |
push @warnings, $_[0]; |
822 |
}; |
823 |
$marc_data = $record->as_usmarc(); |
824 |
} |
825 |
if (@warnings) { |
826 |
# Suppress warnings if record length exceeded |
827 |
unless (substr($record->leader(), 0, 5) eq '99999') { |
828 |
foreach my $warning (@warnings) { |
829 |
carp $warning; |
830 |
} |
831 |
} |
832 |
return (MARC::File::XML::record($record, $marcflavour), 'MARCXML'); |
833 |
} |
834 |
else { |
835 |
if ($format eq 'base64ISO2709') { |
836 |
$marc_data = encode_base64(encode('UTF-8', $marc_data)); |
837 |
} |
838 |
return ($marc_data, $format); |
839 |
} |
840 |
} |
841 |
elsif ($format eq 'MARCXML') { |
842 |
return (MARC::File::XML::record($record, $marcflavour), $format); |
843 |
} |
844 |
else { |
845 |
# This should be unlikely to happen |
846 |
croak "Invalid marc record serialization format: $format"; |
847 |
} |
848 |
} |
849 |
|
850 |
=head2 search_document_marc_record_decode |
851 |
my $marc_record = $self->search_document_marc_record_decode(@result); |
852 |
|
853 |
Extract marc data from Elasticsearch result and decode to MARC::Record object |
854 |
|
855 |
=cut |
856 |
|
857 |
sub search_document_marc_record_decode { |
858 |
# Result is passed in as array, will get flattened |
859 |
# and first element will be $result |
860 |
my ($self, $result) = @_; |
861 |
if ($result->{marc_format} eq 'base64ISO2709') { |
862 |
return MARC::Record->new_from_usmarc(decode_base64($result->{marc_data})); |
863 |
} |
864 |
elsif ($result->{marc_format} eq 'MARCXML') { |
865 |
return MARC::Record->new_from_xml($result->{marc_data}, 'UTF-8', uc C4::Context->preference('marcflavour')); |
866 |
} |
867 |
elsif ($result->{marc_format} eq 'ARRAY') { |
868 |
return $self->_array_to_marc($result->{marc_data_array}); |
869 |
} |
870 |
else { |
871 |
Koha::Exceptions::Elasticsearch->throw("Missing marc_format field in Elasticsearch result"); |
872 |
} |
873 |
} |
874 |
|
875 |
=head2 search_documents_encode($docs, $preferred_format) |
876 |
|
877 |
$records_data = $self->search_documents_encode($docs, $preferred_format) |
878 |
|
879 |
Return marc encoded records from ElasticSearch search result documents. The return value |
880 |
C<$marc_records> is a hashref with encoded records keyed by MARC format. |
881 |
|
882 |
=over 4 |
883 |
|
884 |
=item C<$docs> |
885 |
|
886 |
An arrayref of Elasticsearch search documents |
887 |
|
888 |
=item C<$preferred_format> |
889 |
|
890 |
The preferred marc format: 'MARCXML' or 'ISO2709'. Records exceeding maximum |
891 |
length supported by ISO2709 will be exported as 'MARCXML' even if C<$preferred_format> |
892 |
is set to 'ISO2709'. |
893 |
|
894 |
=back |
895 |
|
896 |
=cut |
897 |
|
898 |
sub search_documents_encode { |
899 |
|
900 |
my ($self, $docs, $preferred_format) = @_; |
901 |
|
902 |
my %encoded_records = ( |
903 |
'ISO2709' => [], |
904 |
'MARCXML' => [] |
905 |
); |
906 |
|
907 |
unless (exists $encoded_records{$preferred_format}) { |
908 |
croak "Invalid preferred format: $preferred_format"; |
909 |
} |
910 |
|
911 |
for my $es_record (@{$docs}) { |
912 |
# Special optimized cases |
913 |
my $marc_data; |
914 |
my $resulting_format = $preferred_format; |
915 |
if ($preferred_format eq 'MARCXML' && $es_record->{_source}{marc_format} eq 'MARCXML') { |
916 |
$marc_data = $es_record->{_source}{marc_data}; |
917 |
} |
918 |
elsif ($preferred_format eq 'ISO2709' && $es_record->{_source}->{marc_format} eq 'base64ISO2709') { |
919 |
$marc_data = decode_base64($es_record->{_source}->{marc_data}); |
920 |
} |
921 |
else { |
922 |
my $record = $self->search_document_marc_record_decode($es_record->{'_source'}); |
923 |
my $marcflavour = lc C4::Context->preference('marcflavour'); |
924 |
($marc_data, $resulting_format) = $self->search_document_marc_record_encode($record, $preferred_format, $marcflavour); |
925 |
} |
926 |
push @{$encoded_records{$resulting_format}}, $marc_data; |
927 |
} |
928 |
if (@{$encoded_records{'ISO2709'}}) { |
929 |
$encoded_records{'ISO2709'} = join("", @{$encoded_records{'ISO2709'}}); |
930 |
} |
931 |
else { |
932 |
delete $encoded_records{'ISO2709'}; |
933 |
} |
934 |
|
935 |
if (@{$encoded_records{'MARCXML'}}) { |
936 |
$encoded_records{'MARCXML'} = encode( |
937 |
'UTF-8', |
938 |
join( |
939 |
"\n", |
940 |
MARC::File::XML::header(), |
941 |
join("\n", @{$encoded_records{'MARCXML'}}), |
942 |
MARC::File::XML::footer() |
943 |
) |
944 |
); |
945 |
} |
946 |
else { |
947 |
delete $encoded_records{'MARCXML'}; |
948 |
} |
949 |
|
950 |
return \%encoded_records; |
951 |
} |
952 |
|
953 |
=head2 search_result_export_custom_formats() |
954 |
|
955 |
$custom_formats = $self->search_result_export_custom_formats() |
956 |
|
957 |
Return user defined custom search result export formats. |
958 |
|
959 |
=cut |
960 |
|
961 |
sub search_result_export_custom_formats { |
962 |
my $export_custom_formats_pref = C4::Context->yaml_preference('ElasticsearchSearchResultExportCustomFormats') || []; |
963 |
my $custom_export_formats = {}; |
964 |
|
965 |
if (ref $export_custom_formats_pref eq 'ARRAY') { |
966 |
for (my $i = 0; $i < @{$export_custom_formats_pref}; ++$i) { |
967 |
# TODO: Perhaps validate on save or trow error here instead of just |
968 |
# ignoring invalid formats |
969 |
my $format = $export_custom_formats_pref->[$i]; |
970 |
if ( |
971 |
ref $format->{fields} eq 'ARRAY' && |
972 |
@{$format->{fields}} && |
973 |
$format->{name} |
974 |
) { |
975 |
$format->{multiple} = 'ignore' unless exists $format->{multiple}; |
976 |
$custom_export_formats->{"custom_$i"} = $format; |
977 |
} |
978 |
} |
979 |
} |
980 |
return $custom_export_formats; |
981 |
} |
982 |
|
983 |
=head2 search_documents_custom_format_encode($docs, $custom_format) |
984 |
|
985 |
$records_data = $self->search_documents_custom_format_encode($docs, $custom_format) |
986 |
|
987 |
Return encoded records from ElasticSearch search result documents using a |
988 |
custom format defined in the "ElasticsearchSearchResultExportCustomFormats" syspref. |
989 |
Returns the encoded records. |
990 |
|
991 |
=over 4 |
992 |
|
993 |
=item C<$docs> |
994 |
|
995 |
An arrayref of Elasticsearch search documents |
996 |
|
997 |
=item C<$format> |
998 |
|
999 |
A hashref with the custom format definition. |
1000 |
|
1001 |
=back |
1002 |
|
1003 |
=cut |
1004 |
|
1005 |
sub search_documents_custom_format_encode { |
1006 |
my ($self, $docs, $format) = @_; |
1007 |
|
1008 |
my $result; |
1009 |
|
1010 |
my $doc_get_fields = sub { |
1011 |
my ($doc, $fields) = @_; |
1012 |
my @row; |
1013 |
foreach my $field (@{$fields}) { |
1014 |
my $values = $doc->{_source}->{$field}; |
1015 |
push @row, ref $values eq 'ARRAY' ? $values : ['']; |
1016 |
} |
1017 |
return \@row; |
1018 |
}; |
1019 |
|
1020 |
my @rows = map { $doc_get_fields->($_, $format->{fields}) } @{$docs}; |
1021 |
|
1022 |
if($format->{multiple} eq 'ignore') { |
1023 |
for (my $i = 0; $i < @rows; ++$i) { |
1024 |
$rows[$i] = [map { $_->[0] } @{$rows[$i]}]; |
1025 |
} |
1026 |
} |
1027 |
elsif($format->{multiple} eq 'newline') { |
1028 |
if (@{$format->{fields}} == 1) { |
1029 |
@rows = map { [join("\n", @{$_->[0]})] } @rows; |
1030 |
} |
1031 |
else { |
1032 |
croak "'newline' is only valid for single field export formats"; |
1033 |
} |
1034 |
} |
1035 |
elsif($format->{multiple} eq 'join') { |
1036 |
for (my $i = 0; $i < @rows; ++$i) { |
1037 |
# Escape separator |
1038 |
for (my $j = 0; $j < @{$rows[$i]}; ++$j) { |
1039 |
for (my $k = 0; $k < @{$rows[$i][$j]}; ++$k) { |
1040 |
$rows[$i][$j][$k] =~ s/\|/\\|/g; |
1041 |
} |
1042 |
} |
1043 |
# Separate multiple values with "|" |
1044 |
$rows[$i] = [map { join("|", @{$_}) } @{$rows[$i]}]; |
1045 |
} |
1046 |
} |
1047 |
else { |
1048 |
croak "Invalid 'multiple' option: " . $format->{multiple}; |
1049 |
} |
1050 |
if (@{$format->{fields}} == 1) { |
1051 |
@rows = grep { $_ ne '' } map { $_->[0] } @rows; |
1052 |
} |
1053 |
else { |
1054 |
# Encode CSV |
1055 |
for (my $i = 0; $i < @rows; ++$i) { |
1056 |
# Escape quotes |
1057 |
for (my $j = 0; $j < @{$rows[$i]}; ++$j) { |
1058 |
$rows[$i][$j] =~ s/"/""/g; |
1059 |
} |
1060 |
$rows[$i] = join(',', map { "\"$_\"" } @{$rows[$i]}); |
1061 |
} |
1062 |
} |
1063 |
|
1064 |
return encode('UTF-8', join("\n", @rows)); |
1065 |
} |
1066 |
|
802 |
=head2 _marc_to_array($record) |
1067 |
=head2 _marc_to_array($record) |
803 |
|
1068 |
|
804 |
my @fields = _marc_to_array($record) |
1069 |
my @fields = _marc_to_array($record) |
Lines 870-887
sub _array_to_marc {
Link Here
|
870 |
$record->leader($data->{leader}); |
1135 |
$record->leader($data->{leader}); |
871 |
for my $field (@{$data->{fields}}) { |
1136 |
for my $field (@{$data->{fields}}) { |
872 |
my $tag = (keys %{$field})[0]; |
1137 |
my $tag = (keys %{$field})[0]; |
873 |
$field = $field->{$tag}; |
1138 |
my $field_data = $field->{$tag}; |
874 |
my $marc_field; |
1139 |
my $marc_field; |
875 |
if (ref($field) eq 'HASH') { |
1140 |
if (ref($field_data) eq 'HASH') { |
876 |
my @subfields; |
1141 |
my @subfields; |
877 |
foreach my $subfield (@{$field->{subfields}}) { |
1142 |
foreach my $subfield (@{$field_data->{subfields}}) { |
878 |
my $code = (keys %{$subfield})[0]; |
1143 |
my $code = (keys %{$subfield})[0]; |
879 |
push @subfields, $code; |
1144 |
push @subfields, $code; |
880 |
push @subfields, $subfield->{$code}; |
1145 |
push @subfields, $subfield->{$code}; |
881 |
} |
1146 |
} |
882 |
$marc_field = MARC::Field->new($tag, $field->{ind1}, $field->{ind2}, @subfields); |
1147 |
$marc_field = MARC::Field->new($tag, $field_data->{ind1}, $field_data->{ind2}, @subfields); |
883 |
} else { |
1148 |
} else { |
884 |
$marc_field = MARC::Field->new($tag, $field) |
1149 |
$marc_field = MARC::Field->new($tag, $field_data) |
885 |
} |
1150 |
} |
886 |
$record->append_fields($marc_field); |
1151 |
$record->append_fields($marc_field); |
887 |
} |
1152 |
} |