Bugzilla – Attachment 47417 Details for
Bug 13937
Add an Elasticsearch-compatible Z39.50/SRU daemon
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 13937 - Add a Z39.50 daemon that can inject item status MARC subfields
Bug-13937---Add-a-Z3950-daemon-that-can-inject-ite.patch (text/plain), 19.48 KB, created by
Jesse Weaver
on 2016-01-28 22:12:35 UTC
(
hide
)
Description:
Bug 13937 - Add a Z39.50 daemon that can inject item status MARC subfields
Filename:
MIME Type:
Creator:
Jesse Weaver
Created:
2016-01-28 22:12:35 UTC
Size:
19.48 KB
patch
obsolete
>From 4f5a87b86ec0ec0118ed73f92753b3951126428f Mon Sep 17 00:00:00 2001 >From: Jesse Weaver <pianohacker@gmail.com> >Date: Fri, 11 Dec 2015 14:26:20 -0700 >Subject: [PATCH] Bug 13937 - Add a Z39.50 daemon that can inject item status > MARC subfields > >This creates a new daemon, misc/z3950_responder.pl, which can respond to >Z39.50 requests. By default, it just proxies searches to Zebra. > >If desired, however, it can also add a subfield to the item tags on >outgoing records with a textual description of the item's status >(checked out, lost, etc.). This is useful for certain ILL systems. These >strings can be translated using the 'Z3950_STATUS' authorized value. > >Test plan: > 1) Start the Z39.50 server using `perl misc/z3950_responder.pl`. > 2) Connect to the server using `yaz-client 127.0.0.1:9999/biblios`. > 3) Run a search, such as `find @attr 1=1016 book`. > 4) Fetch the results both one at a time with `show 1` and in a batch > using `show 1+5`. > 5) Turn on MARCXML using `format xml` and `elements marcxml`, and > verify that the records are still correctly fetched. > 6) Enable the item status subfield by restarting the server with the > option `--add-item-status=k`. > 7) Search for and fetch records, and verify that a $k subfield is > added to the item tags as appropriate. It should show some > combination of "Checked Out", "Lost", "Not For Loan", "Damaged", > "Withdrawn", "In Transit", or "On Hold" as appropriate, or > "Available". > 8) Restart the server with the option `--add-status-multi-subfield`; > now, if there are multiple statuses for a given item, they should > be in multiple $k subfields. > 9) Add an authorized value named "Z3950_STATUS" with any of the keys > "AVAILABLE", "CHECKED_OUT", "LOST", "NOT_FOR_LOAN", "DAMAGED", > "WITHDRAWN", "IN_TRANSIT" or "ON_HOLD", and verify that their > descriptions are used instead of the default values above. >--- > C4/Installer/PerlDependencies.pm | 5 + > Koha/Logger.pm | 24 ++ > etc/log4perl.conf | 7 + > misc/z3950_responder.pl | 510 +++++++++++++++++++++++++++++++++++++++ > 4 files changed, 546 insertions(+) > create mode 100755 misc/z3950_responder.pl > >diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm >index c1d03c7..b11343d 100644 >--- a/C4/Installer/PerlDependencies.pm >+++ b/C4/Installer/PerlDependencies.pm >@@ -782,6 +782,11 @@ our $PERL_DEPS = { > 'required' => '1', > 'min_ver' => '1.10', > }, >+ 'Net::Z3950::SimpleServer' => { >+ 'usage' => 'Z39.50 responder', >+ 'required' => '0', >+ 'min_ver' => '1.15', >+ }, > }; > > 1; >diff --git a/Koha/Logger.pm b/Koha/Logger.pm >index b454100..200b9a2 100644 >--- a/Koha/Logger.pm >+++ b/Koha/Logger.pm >@@ -177,6 +177,30 @@ sub _recheck_logfile { # recheck saved logfile when logging message > return -w $log; > } > >+=head2 debug_to_screen >+ >+Adds a new appender for the given logger that will log all DEBUG-and-higher messages to stderr. >+Useful for daemons. >+ >+=cut >+ >+sub debug_to_screen { >+ my $self = shift; >+ >+ return unless ( $self->{logger} ); >+ >+ my $appender = Log::Log4perl::Appender->new( >+ 'Log::Log4perl::Appender::Screen', >+ stderr => 1, >+ utf8 => 1, >+ name => 'debug_to_screen' # We need a specific name to prevent duplicates >+ ); >+ >+ $appender->threshold( $Log::Log4perl::DEBUG ); >+ $self->{logger}->add_appender( $appender ); >+ $self->{logger}->level( $Log::Log4perl::DEBUG ); >+} >+ > =head1 AUTHOR > > Kyle M Hall, E<lt>kyle@bywatersolutions.comE<gt> >diff --git a/etc/log4perl.conf b/etc/log4perl.conf >index efc4707..48601fb 100644 >--- a/etc/log4perl.conf >+++ b/etc/log4perl.conf >@@ -11,3 +11,10 @@ log4perl.appender.OPAC.filename=__LOG_DIR__/opac-error.log > log4perl.appender.OPAC.mode=append > log4perl.appender.OPAC.layout=PatternLayout > log4perl.appender.OPAC.layout.ConversionPattern=[%d] [%p] %m %l %n >+ >+log4perl.logger.z3950 = WARN, Z3950 >+log4perl.appender.Z3950=Log::Log4perl::Appender::File >+log4perl.appender.Z3950.filename=__LOG_DIR__/logs/z3950-error.log >+log4perl.appender.Z3950.mode=append >+log4perl.appender.Z3950.layout=PatternLayout >+log4perl.appender.Z3950.layout.ConversionPattern=[%d] [%p] %m %l %n >diff --git a/misc/z3950_responder.pl b/misc/z3950_responder.pl >new file mode 100755 >index 0000000..c45c779 >--- /dev/null >+++ b/misc/z3950_responder.pl >@@ -0,0 +1,510 @@ >+#!/usr/bin/perl >+# >+# Copyright ByWater Solutions 2015 >+# >+# 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, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use C4::Biblio qw( GetMarcFromKohaField ); >+use C4::Circulation qw( GetTransfers ); >+use C4::Context; >+use C4::Koha qw( GetAuthorisedValues ); >+use C4::Items qw( GetItem ); >+use C4::Reserves qw( GetReserveStatus ); >+use C4::Search qw(); >+use Koha; >+use Koha::Logger; >+ >+use Carp; >+use Getopt::Long; >+use Net::Z3950::SimpleServer; >+use Pod::Usage; >+use ZOOM; >+ >+use constant { >+ UNIMARC_OID => '1.2.840.10003.5.1', >+ USMARC_OID => '1.2.840.10003.5.10', >+ MARCXML_OID => '1.2.840.10003.5.109.10' >+}; >+ >+use constant { >+ ERR_TEMPORARY_ERROR => 2, >+ ERR_PRESENT_OUT_OF_RANGE => 13, >+ ERR_RECORD_TOO_LARGE => 16, >+ ERR_NO_SUCH_RESULTSET => 30, >+ ERR_SYNTAX_UNSUPPORTED => 230, >+ ERR_DB_DOES_NOT_EXIST => 235, >+}; >+ >+=head1 SYNOPSIS >+ >+ z3950_responder.pl [-h|--help] [--man] [-a <pdufile>] [-v <loglevel>] [-l <logfile>] [-u <user>] >+ [-c <config>] [-t <minutes>] [-k <kilobytes>] [-d <daemon>] [-p <pidfile>] >+ [-C certfile] [-zKiDST1] [-m <time-format>] [-w <directory>] [--debug] >+ [--add-item-status=SUBFIELD] [--prefetch=NUM_RECORDS] >+ [<listener-addr>... ] >+ >+=head1 OPTIONS >+ >+=over 8 >+ >+=item B<--help> >+ >+Prints a brief usage message and exits. >+ >+=item B<--man> >+ >+Displays manual page and exits. >+ >+=item B<--debug> >+ >+Turns on debug logging to the screen, and turns on single-process mode. >+ >+=item B<--add-item-status=SUBFIELD> >+ >+If given, adds item status information to the given subfield. >+ >+=item B<--add-status-multi-subfield> >+ >+With the above, instead of putting multiple item statuses in one subfield, adds a subfield for each >+status string. >+ >+=item B<--prefetch=NUM_RECORDS> >+ >+Number of records to prefetch from Zebra. Defaults to 20. >+ >+=back >+ >+=head1 CONFIGURATION >+ >+The item status strings added by B<--add-item-status> can be configured with the B<Z3950_STATUS> >+authorized value, using the following keys: >+ >+=over 4 >+ >+=item AVAILABLE >+ >+=item CHECKED_OUT >+ >+=item LOST >+ >+=item NOT_FOR_LOAN >+ >+=item DAMAGED >+ >+=item WITHDRAWN >+ >+=item IN_TRANSIT >+ >+=item ON_HOLD >+ >+=back >+ >+=cut >+ >+my $add_item_status_subfield; >+my $add_status_multi_subfield; >+my $debug = 0; >+my $help; >+my $man; >+my $prefetch = 20; >+my @yaz_options; >+ >+sub add_yaz_option { >+ my ( $opt_name, $opt_value ) = @_; >+ >+ push @yaz_options, "-$opt_name", "$opt_value"; >+} >+ >+GetOptions( >+ '-h|help' => \$help, >+ '--man' => \$man, >+ '--debug' => \$debug, >+ '--add-item-status=s' => \$add_item_status_subfield, >+ '--add-status-multi-subfield' => \$add_status_multi_subfield, >+ '--prefetch=i' => \$prefetch, >+ # Pass through YAZ options. >+ 'a=s' => \&add_yaz_option, >+ 'v=s' => \&add_yaz_option, >+ 'l=s' => \&add_yaz_option, >+ 'u=s' => \&add_yaz_option, >+ 'c=s' => \&add_yaz_option, >+ 't=s' => \&add_yaz_option, >+ 'k=s' => \&add_yaz_option, >+ 'd=s' => \&add_yaz_option, >+ 'p=s' => \&add_yaz_option, >+ 'C=s' => \&add_yaz_option, >+ 'm=s' => \&add_yaz_option, >+ 'w=s' => \&add_yaz_option, >+ 'z' => \&add_yaz_option, >+ 'K' => \&add_yaz_option, >+ 'i' => \&add_yaz_option, >+ 'D' => \&add_yaz_option, >+ 'S' => \&add_yaz_option, >+ 'T' => \&add_yaz_option, >+ '1' => \&add_yaz_option >+) || pod2usage(2); >+ >+pod2usage(1) if $help; >+pod2usage( -verbose => 2 ) if $man; >+ >+unshift @ARGV, @yaz_options; >+ >+# Turn off Yaz's built-in logging (can be turned back on if desired). >+unshift @ARGV, '-v', 'none'; >+ >+# If requested, turn on debugging. >+ >+if ( $debug ) { >+ # Turn on single-process mode. >+ unshift @ARGV, '-S'; >+} >+ >+my ($item_tag, $itemnumber_subfield) = GetMarcFromKohaField("items.itemnumber",''); >+ >+# We hardcode the strings for English so SOMETHING will work if the authorized value doesn't exist. >+ >+my $status_strings = { >+ AVAILABLE => 'Available', >+ CHECKED_OUT => 'Checked Out', >+ LOST => 'Lost', >+ NOT_FOR_LOAN => 'Not for Loan', >+ DAMAGED => 'Damaged', >+ WITHDRAWN => 'Withdrawn', >+ IN_TRANSIT => 'In Transit', >+ ON_HOLD => 'On Hold', >+}; >+ >+foreach my $val ( @{ GetAuthorisedValues( 'Z3950_STATUS' ) } ) { >+ $status_strings->{ $val->{authorised_value} } = $val->{lib}; >+} >+ >+# Create and start the server. >+ >+my $z = Net::Z3950::SimpleServer->new( >+ # Global context object. This is shared between processes, so it should not be modified. >+ GHANDLE => { >+ add_item_status_subfield => $add_item_status_subfield, >+ add_status_multi_subfield => $add_status_multi_subfield, >+ debug => $debug, >+ item_tag => $item_tag, >+ itemnumber_subfield => $itemnumber_subfield, >+ num_to_prefetch => $prefetch, >+ status_strings => $status_strings, >+ }, >+ INIT => \&init_handler, >+ SEARCH => \&search_handler, >+ PRESENT => \&present_handler, >+ FETCH => \&fetch_handler, >+ CLOSE => \&close_handler, >+); >+ >+$z->launch_server('z3950_responder.pl', @ARGV); >+ >+sub _set_error { >+ my ( $args, $code, $msg ) = @_; >+ ( $args->{ERR_CODE}, $args->{ERR_STR} ) = ( $code, $msg ); >+ >+ $args->{HANDLE}->{logger}->info("[$args->{HANDLE}->{peer}] returning error $code: $msg"); >+} >+ >+sub _set_error_from_zoom { >+ my ( $args, $exception ) = @_; >+ >+ _set_error( $args, ERR_TEMPORARY_ERROR, 'Cannot connect to upstream server' ); >+ $args->{HANDLE}->{logger}->error( >+ "Zebra upstream error: " . >+ $exception->message() . " (" . >+ $exception->code() . ") " . >+ ( $exception->addinfo() // '' ) . " " . >+ $exception->diagset() >+ ); >+} >+ >+# This code originally went through C4::Search::getRecords, but had to use so many escape hatches >+# that it was easier to directly connect to Zebra. >+sub _start_search { >+ my ( $config, $session, $args, $in_retry ) = @_; >+ >+ my $database = $args->{DATABASES}->[0]; >+ my ( $connection, $results ); >+ >+ eval { >+ $connection = C4::Context->Zconn( >+ # We're depending on the caller to have done some validation. >+ $database eq 'biblios' ? 'biblioserver' : 'authorityserver', >+ 0 # No, no async, doesn't really help much for single-server searching >+ ); >+ >+ $results = $connection->search_pqf( $args->{QUERY} ); >+ >+ $session->{logger}->debug("[$session->{peer}] retry successful") if ($in_retry); >+ }; >+ if ($@) { >+ die $@ if ( ref($@) ne 'ZOOM::Exception' ); >+ >+ if ( $@->diagset() eq 'ZOOM' && $@->code() == 10004 && !$in_retry ) { >+ $session->{logger}->debug("[$session->{peer}] upstream server lost connection, retrying"); >+ return _start_search( $config, $session, $args, $in_retry ); >+ } >+ >+ _set_error_from_zoom( $args, $@ ); >+ $connection = undef; >+ } >+ >+ return ( $connection, $results, $results ? $results->size() : -1 ); >+} >+ >+sub _prefetch_records { >+ my ( $session, $resultset, $args, $start, $num_records ) = @_; >+ >+ eval { >+ if ( $start + $num_records >= $resultset->{results}->size() ) { >+ $num_records = $resultset->{results}->size() - $start; >+ } >+ >+ $session->{logger}->debug("[$session->{peer}] prefetching $num_records records starting at $start"); >+ >+ $resultset->{results}->records( $start, $num_records, 0 ); >+ }; >+ if ($@) { >+ die $@ if ( ref($@) ne 'ZOOM::Exception' ); >+ _set_error_from_zoom( $args, $@ ); >+ return; >+ } >+} >+ >+sub _fetch_record { >+ my ( $session, $resultset, $args, $index, $num_to_prefetch ) = @_; >+ >+ my $record; >+ >+ eval { >+ if ( !$resultset->{results}->record_immediate( $index ) ) { >+ my $start = int( $index / $num_to_prefetch ) * $num_to_prefetch; >+ >+ if ( $start + $num_to_prefetch >= $resultset->{results}->size() ) { >+ $num_to_prefetch = $resultset->{results}->size() - $start; >+ } >+ >+ $session->{logger}->debug("[$session->{peer}] fetch uncached, fetching $num_to_prefetch records starting at $start"); >+ >+ $resultset->{results}->records( $start, $num_to_prefetch, 0 ); >+ } >+ >+ $record = $resultset->{results}->record_immediate( $index )->raw(); >+ }; >+ if ($@) { >+ die $@ if ( ref($@) ne 'ZOOM::Exception' ); >+ _set_error_from_zoom( $args, $@ ); >+ return; >+ } else { >+ return $record; >+ } >+} >+ >+sub _close_resultset { >+ my ( $resultset ) = @_; >+ >+ $resultset->{results}->destroy(); >+ # We can't destroy the connection, as it may be cached >+} >+ >+sub init_handler { >+ # Called when the client first connects. >+ my ( $args ) = @_; >+ >+ my $config = $args->{GHANDLE}; >+ >+ # This holds all of the per-connection state. >+ my $session = { >+ logger => Koha::Logger->get({ interface => 'z3950' }), >+ peer => $args->{PEER_NAME}, >+ resultsets => {}, >+ }; >+ >+ if ( $config->{debug} ) { >+ $session->{logger}->debug_to_screen(); >+ } >+ >+ $args->{HANDLE} = $session; >+ >+ $args->{IMP_NAME} = "Koha"; >+ $args->{IMP_VER} = Koha::version; >+ >+ $session->{logger}->info("[$session->{peer}] connected"); >+} >+ >+sub search_handler { >+ # Called when search is first sent. >+ my ( $args ) = @_; >+ >+ my $database = $args->{DATABASES}->[0]; >+ if ( $database !~ /^(biblios|authorities)$/ ) { >+ _set_error( ERR_DB_DOES_NOT_EXIST, 'No such database' ); >+ return; >+ } >+ >+ my $config = $args->{GHANDLE}; >+ my $session = $args->{HANDLE}; >+ >+ my $query = $args->{QUERY}; >+ $session->{logger}->info("[$session->{peer}] received search for '$query', (RS $args->{SETNAME})"); >+ >+ my ( $connection, $results, $num_hits ) = _start_search( $config, $session, $args ); >+ return unless $connection; >+ >+ $args->{HITS} = $num_hits; >+ my $resultset = $session->{resultsets}->{ $args->{SETNAME} } = { >+ database => $database, >+ connection => $connection, >+ results => $results, >+ query => $args->{QUERY}, >+ hits => $args->{HITS}, >+ }; >+} >+ >+sub _check_fetch { >+ my ( $args, $resultset, $offset, $num_records ) = @_; >+ >+ if ( !defined( $resultset ) ) { >+ _set_error( $args, ERR_NO_SUCH_RESULTSET, 'No such resultset' ); >+ return 0; >+ } >+ >+ if ( $offset + $num_records > $resultset->{hits} ) { >+ _set_error( $args, ERR_PRESENT_OUT_OF_RANGE, 'Fetch request out of range' ); >+ return 0; >+ } >+ >+ return 1; >+} >+ >+sub present_handler { >+ # Called when a set of records is requested. >+ my ( $args ) = @_; >+ my $session = $args->{HANDLE}; >+ >+ $session->{logger}->debug("[$session->{peer}] received present for $args->{SETNAME}, $args->{START}+$args->{NUMBER}"); >+ >+ my $resultset = $session->{resultsets}->{ $args->{SETNAME} }; >+ # The offset comes across 1-indexed. >+ my $offset = $args->{START} - 1; >+ >+ return unless _check_fetch( $args, $resultset, $offset, $args->{NUMBER} ); >+ >+ # Ignore if request is only for one record; our own prefetching will probably do a better job. >+ _prefetch_records( $session, $resultset, $args, $offset, $args->{NUMBER} ) if ( $args->{NUMBER} > 1 ); >+} >+ >+sub fetch_handler { >+ # Called when a given record is requested. >+ my ( $args ) = @_; >+ my $config = $args->{GHANDLE}; >+ my $session = $args->{HANDLE}; >+ >+ $session->{logger}->debug("[$session->{peer}] received fetch for $args->{SETNAME}, record $args->{OFFSET}"); >+ my $form_oid = $args->{REQ_FORM} // ''; >+ my $composition = $args->{COMP} // ''; >+ $session->{logger}->debug("[$session->{peer}] form OID $form_oid, composition $composition"); >+ >+ my $resultset = $session->{resultsets}->{ $args->{SETNAME} }; >+ # The offset comes across 1-indexed. >+ my $offset = $args->{OFFSET} - 1; >+ >+ return unless _check_fetch( $args, $resultset, $offset, 1 ); >+ >+ $args->{LAST} = 1 if ( $offset == $resultset->{hits} - 1 ); >+ >+ my $record = _fetch_record( $session, $resultset, $args, $offset, $config->{num_to_prefetch} ); >+ return unless $record; >+ >+ $record = C4::Search::new_record_from_zebra( >+ $resultset->{database} eq 'biblios' ? 'biblioserver' : 'authorityserver', >+ $record >+ ); >+ >+ if ( $config->{add_item_status_subfield} ) { >+ my $tag = $config->{item_tag}; >+ my $itemnumber_subfield = $config->{itemnumber_subfield}; >+ my $add_subfield = $config->{add_item_status_subfield}; >+ my $status_strings = $config->{status_strings}; >+ >+ foreach my $field ( $record->field($tag) ) { >+ my $itemnumber = $field->subfield($itemnumber_subfield); >+ next unless $itemnumber; >+ >+ my $item = GetItem( $itemnumber ); >+ my @statuses; >+ >+ if ( $item->{onloan} ) { >+ push @statuses, $status_strings->{CHECKED_OUT}; >+ } >+ >+ if ( $item->{itemlost} ) { >+ push @statuses, $status_strings->{LOST}; >+ } >+ >+ if ( $item->{notforloan} ) { >+ push @statuses, $status_strings->{NOT_FOR_LOAN}; >+ } >+ >+ if ( $item->{damaged} ) { >+ push @statuses, $status_strings->{DAMAGED}; >+ } >+ >+ if ( $item->{withdrawn} ) { >+ push @statuses, $status_strings->{WITHDRAWN}; >+ } >+ >+ if ( scalar( GetTransfers( $itemnumber ) ) ) { >+ push @statuses, $status_strings->{IN_TRANSIT}; >+ } >+ >+ if ( GetReserveStatus( $itemnumber ) ne '' ) { >+ push @statuses, $status_strings->{ON_HOLD}; >+ } >+ >+ $field->delete_subfield( code => $itemnumber_subfield ); >+ >+ if ( $config->{add_status_multi_subfield} ) { >+ $field->add_subfields( map { ( $add_subfield, $_ ) } ( @statuses ? @statuses : $status_strings->{AVAILABLE} ) ); >+ } else { >+ $field->add_subfields( $add_subfield, @statuses ? join( ', ', @statuses ) : $status_strings->{AVAILABLE} ); >+ } >+ } >+ } >+ >+ if ( $form_oid eq MARCXML_OID && $composition eq 'marcxml' ) { >+ $args->{RECORD} = $record->as_xml_record(); >+ } elsif ( ( $form_oid eq USMARC_OID || $form_oid eq UNIMARC_OID ) && ( !$composition || $composition eq 'F' ) ) { >+ $args->{RECORD} = $record->as_usmarc(); >+ } else { >+ _set_error( $args, ERR_SYNTAX_UNSUPPORTED, "Unsupported syntax/composition $form_oid/$composition" ); >+ return; >+ } >+} >+ >+sub close_handler { >+ my ( $args ) = @_; >+ >+ my $session = $args->{HANDLE}; >+ >+ foreach my $resultset ( values %{ $session->{resultsets} } ) { >+ _close_resultset( $resultset ); >+ } >+} >-- >2.6.2
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 13937
:
46369
|
47417
|
47418
|
47419
|
53503
|
58122
|
58125
|
58126
|
59443
|
59444
|
60735
|
60736
|
60737
|
60738
|
66943
|
66944
|
66945
|
66946
|
69139
|
82562
|
82563
|
82564
|
82565
|
82566
|
82567
|
82589
|
82590
|
82591
|
82592
|
82593
|
82594
|
82595
|
82660
|
82661
|
82662
|
82663
|
82664
|
82665
|
82666
|
82667
|
83780
|
83781
|
83782
|
83783
|
83784
|
83785
|
83786
|
83787
|
83788
|
83789
|
83951
|
83952
|
83953
|
83954
|
83955
|
83956
|
83957
|
83958
|
83959
|
84050
|
84051
|
84052
|
84053
|
84054
|
84055
|
84056
|
84057
|
84058
|
84059
|
84062
|
84063
|
84064
|
84065
|
84066
|
84067
|
84068
|
84069
|
84072
|
84073
|
85112
|
86353
|
86354
|
86355
|
86356
|
86357
|
86358
|
86359
|
86360
|
86361
|
86362
|
86363
|
86364
|
88907
|
88908
|
88909
|
88910
|
88911
|
88912
|
88913
|
88914
|
88915
|
88916
|
88917
|
88918
|
91728
|
91729
|
91730
|
91731
|
91732
|
91733
|
91734
|
91735
|
91736
|
91737
|
91738
|
91739
|
91740
|
93913
|
94097
|
94109
|
94110
|
94209
|
94616
|
94647