From 6c668e8a5361454496f0758c0df9a2deeb705547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 15:27:12 +0100 Subject: [PATCH 01/31] Ignore temporal and output files. --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a0f29b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Vim temp files. +.*.swp + +# Output files +api_adapter.cgi +get_aleph_info.csh +sql_lookup.cgi +sql_lookup.csh +sql_lookup.txt From 98bdd83cf42b321ec769443f4ca6dca6f37d3086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 14:57:28 +0100 Subject: [PATCH 02/31] Do not consider X-server working unless HTTP response is correct. --- validate.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate.pl b/validate.pl index c274093..7ab8f61 100755 --- a/validate.pl +++ b/validate.pl @@ -54,7 +54,7 @@ BEGIN my $x_url = "http://localhost/X?op=bor_by_key&bor_id=$aleph_id"; print "Sending $x_url\n"; my $response = $ua->get($x_url); -if (grep //, $response->content) { +if (not $response->is_success or grep //, $response->content) { print "X-server retrieval status:\n\t[failed]\n"; } else { From 04dca0d5000c2432c919d540ab8b756dae383517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 15:09:11 +0100 Subject: [PATCH 03/31] Do not consider REST API working unless HTTP response is correct. --- validate.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate.pl b/validate.pl index 7ab8f61..b1fe1a3 100755 --- a/validate.pl +++ b/validate.pl @@ -68,7 +68,7 @@ BEGIN $rest_url =~ s/PORT/$jboss_port/g; print "\nSending $rest_url\n"; $response = $ua->get($rest_url); -if (!grep /0000<\/reply-code>/, $response->content) { +if (not $response->is_success or !grep /0000<\/reply-code>/, $response->content) { print "REST API retrieval status:\n\t[failed]\n"; } else { From 1a201162af6d66e0a24bd61810de878ef52d2d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 15:06:15 +0100 Subject: [PATCH 04/31] Validate validate.pl command line arguments. --- validate.pl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/validate.pl b/validate.pl index b1fe1a3..4d432af 100755 --- a/validate.pl +++ b/validate.pl @@ -9,6 +9,14 @@ my $aleph_id = $ARGV[0]; my $jboss_port = $ARGV[1]; +die 'No correct Aleph ID was specified.' + unless (defined($aleph_id) + and $aleph_id =~ /^\S{1,12}$/); +die 'No correct JBoss port was specified.' + unless (defined($jboss_port) + and $jboss_port =~ /^\d+$/ + and $jboss_port > 0 and $jboss_port < 65536); + BEGIN { #------------------------------------------------ # Read the file created by install_adapter.pl. From 89f90e39dc15f5ed1b9f86e435f57b554be684b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 15:19:35 +0100 Subject: [PATCH 05/31] Check $ENV{WWW_HOST} before use. --- validate.pl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/validate.pl b/validate.pl index 4d432af..09fe4fd 100755 --- a/validate.pl +++ b/validate.pl @@ -97,6 +97,9 @@ BEGIN #------------------------------------------------ # Generate a URL for testing the installation. #------------------------------------------------ +die '$ENV{WWW_HOST} not defined.' + unless (defined($ENV{WWW_HOST}) + and $ENV{WWW_HOST} !~ /^\s*$/); my $test_url = "https://$ENV{WWW_HOST}/rest-dlf/patron/$aleph_id/patronInformation/address"; print "\nPut this URL in a browser to test the adapter after all the installation steps are completed.\n\t$test_url\n\n"; exit; From 0f14d80fe033184d5c42d2e94bac35c423dd1bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 15:33:52 +0100 Subject: [PATCH 06/31] =?UTF-8?q?Fix=20calling=20of=20=E2=80=98validate.pl?= =?UTF-8?q?=E2=80=99=20from=20=E2=80=98install=5Fadapter.pl=E2=80=99.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install_adapter.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install_adapter.pl b/install_adapter.pl index 6ca1801..3b14182 100755 --- a/install_adapter.pl +++ b/install_adapter.pl @@ -151,7 +151,7 @@ # Validate the installation #---------------------------- print "Validating the installation\n"; -my @messages = `validate.pl $aleph_id $jboss_port`; +my @messages = `./validate.pl $aleph_id $jboss_port`; foreach (@messages) { print } exit; From 6c628c1bc456bdea0a2b6f8fe35c140335b59276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 15:36:49 +0100 Subject: [PATCH 07/31] Formatting of outputs. --- validate.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validate.pl b/validate.pl index 09fe4fd..a823b05 100755 --- a/validate.pl +++ b/validate.pl @@ -91,13 +91,13 @@ BEGIN print "\nAleph version retrieval status:\n\t[OK]\n"; } else { - print "Aleph version retrieval status:\n\t[failed]\n"; + print "\nAleph version retrieval status:\n\t[failed]\n"; } #------------------------------------------------ # Generate a URL for testing the installation. #------------------------------------------------ -die '$ENV{WWW_HOST} not defined.' +die "\n".'$ENV{WWW_HOST} not defined.' unless (defined($ENV{WWW_HOST}) and $ENV{WWW_HOST} !~ /^\s*$/); my $test_url = "https://$ENV{WWW_HOST}/rest-dlf/patron/$aleph_id/patronInformation/address"; From e01ba0c2b8119bb71407e57545b4a2bd46153a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 16:08:39 +0100 Subject: [PATCH 08/31] Major source code reformatting. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – All tabs substituted for four spaces. – Fixed indentation. – Added Vim modelines too keep the format seeting for next editings. --- api_adapter.template | 385 ++++++++++++++++++++-------------------- get_aleph_info.template | 2 + install_adapter.pl | 138 +++++++------- sql_lookup.cgi.template | 20 ++- sql_lookup.csh.template | 2 + validate.pl | 94 +++++----- 6 files changed, 323 insertions(+), 318 deletions(-) diff --git a/api_adapter.template b/api_adapter.template index ff78398..b143b31 100755 --- a/api_adapter.template +++ b/api_adapter.template @@ -1,8 +1,8 @@ -#!/usr/bin/perl +#!/usr/bin/perl #-------------------------------------------------------------- # 12-05-2014 Rich Wenger, MIT Libraries -# This script provides Aleph services to an external server. +# This script provides Aleph services to an external server. #-------------------------------------------------------------- use strict; @@ -16,261 +16,262 @@ use Time::Local; my $rest_port = 'PORT'; my @whitelist = (WHITELIST); #----------------------------------------------------------------- -# Only accept connections from authorized IP addresses. +# Only accept connections from authorized IP addresses. #----------------------------------------------------------------- if (!grep /$ENV{REMOTE_ADDR}/, @whitelist) { - print STDERR "*** $0: Unauthorized access attempt from $ENV{REMOTE_ADDR} ***\n"; - print "Content-type: text/html\n\n"; - print "Unathorized access"; - exit; - } + print STDERR "*** $0: Unauthorized access attempt from $ENV{REMOTE_ADDR} ***\n"; + print "Content-type: text/html\n\n"; + print "Unathorized access"; + exit; +} #-------------------------------------------------------------------------------------------- # $debug and $parameter_trace are for diagnostic purposes and will normally be set to 0. # $id_translation will be set to 1 as a default. Setting it to 0 disables the translation -# of alternate identifiers to Aleph ids by the adapter. +# of alternate identifiers to Aleph ids by the adapter. #-------------------------------------------------------------------------------------------- -my $debug = 0; -my $parameter_trace = 0; -my $id_translation = 1; -my $sql_lookup = 0; +my $debug = 0; +my $parameter_trace = 0; +my $id_translation = 1; +my $sql_lookup = 0; #------------------------------------------------------------------------ -# Local base URLs for the Aleph X-server and the RESTful API. +# Local base URLs for the Aleph X-server and the RESTful API. #------------------------------------------------------------------------ -my $x_base_url = 'http://localhost/X?'; -my $r_base_url = "http://localhost:$rest_port"; +my $x_base_url = 'http://localhost/X?'; +my $r_base_url = "http://localhost:$rest_port"; #------------------------------------- -# Headers and XML constants. +# Headers and XML constants. #------------------------------------- -my $xml_header = "Content-type: text/xml\n\n"; +my $xml_header = "Content-type: text/xml\n\n"; my $html_header = "Content-type: text/html\n\n"; -my $xml_prolog = ''; +my $xml_prolog = ''; -my $version_xml = join '', - '', - 'INSTNAME', - 'ALEPHVER', - 'en_US', - 'TIMEZONE', - 'TZCODE', - 'TZGMT', - 'CURRENCY', - ''; +my $version_xml = join '', + '', + 'INSTNAME', + 'ALEPHVER', + 'en_US', + 'TIMEZONE', + 'TZCODE', + 'TZGMT', + 'CURRENCY', + ''; my $printline = ''; my $putdata; my $postdata; #---------------------------------------------------- -# Valid parameters in Aleph RESTful URLs +# Valid parameters in Aleph RESTful URLs #---------------------------------------------------- -my @allowed_groups = ('patron','ilsinstance','record'); -my @allowed_categories = ('patroninformation','circulationactions','record','patronstatus','items','holds'); -my @patinfo_functions = ('address','password'); -my @circ_functions = ('loans','requests','cash'); +my @allowed_groups = ('patron','ilsinstance','record'); +my @allowed_categories = ('patroninformation','circulationactions','record','patronstatus','items','holds'); +my @patinfo_functions = ('address','password'); +my @circ_functions = ('loans','requests','cash'); my @patstatus_functions = ('blocks','registration'); my @allowed_subfunctions = ('holds','photocopies','acquisitionrequests','ill','bookings'); #-------------------------------- -# Valid HTTP methods +# Valid HTTP methods #-------------------------------- -my @allowed_methods = ('get','post','put','delete'); +my @allowed_methods = ('get','post','put','delete'); #---------------------------------------------------------------------------- -# Get the RESTful URL components. -# @parms will contain the RESTful nodes between slashes. -# @args will contain any key=value pairs from the end of the URI. +# Get the RESTful URL components. +# @parms will contain the RESTful nodes between slashes. +# @args will contain any key=value pairs from the end of the URI. #---------------------------------------------------------------------------- my @parms = split /\//, (split /\?parm1=/, lc $ENV{'REQUEST_URI'})[0]; splice @parms,0,2; my @args = split /\&/, (split /\?/, $parms[$#parms])[1]; if (grep /\?/, $parms[$#parms]) { - $parms[$#parms] =~ s/\?(.*)$//go; - } + $parms[$#parms] =~ s/\?(.*)$//go; +} my ($group, $patron_id, $category, $function, $subfunction) = ''; ($group, $patron_id, $category, $function, $subfunction) = @parms; if (!grep /$group/, @allowed_groups) { - print "$html_header invalid group $group"; - exit; - } + print "$html_header invalid group $group"; + exit; +} if (!grep /$category/, @allowed_categories) { - print "$html_header invalid category $category"; - exit; - } + print "$html_header invalid category $category"; + exit; +} #---------------------------------------------------------------------------------------- -# $method will contain one of the HTTP commands: GET, POST, PUT, DELETE, etc. -# They are stored here in lower case for later use as method calls to LWP. +# $method will contain one of the HTTP commands: GET, POST, PUT, DELETE, etc. +# They are stored here in lower case for later use as method calls to LWP. #---------------------------------------------------------------------------------------- my $method = lc $ENV{'REQUEST_METHOD'}; #------------------------------------------------------------------------------ -# This paragraph is for diagnostic purposes only. It writes parameters +# This paragraph is for diagnostic purposes only. It writes parameters # and arguments to the Apache log (STDERR) and exits. #------------------------------------------------------------------------------ if ($parameter_trace) { - print "$html_header"; - foreach my $x (@parms) { print "parm: $x
"; } - if (@args) { - foreach my $x (@args) { print "args: $x
"; } - } - if ($debug) { - my $printline = join '', - "*** Group: $group ***\n", - "*** Patron id: $patron_id ***\n", - "*** Category: $category ***\n", - "*** Function: $function ***\n", - "*** Subfunction: $subfunction ***\n"; - print STDERR $printline; - } - exit; - } + print "$html_header"; + foreach my $x (@parms) { print "parm: $x
"; } + if (@args) { + foreach my $x (@args) { print "args: $x
"; } + } + if ($debug) { + my $printline = join '', + "*** Group: $group ***\n", + "*** Patron id: $patron_id ***\n", + "*** Category: $category ***\n", + "*** Function: $function ***\n", + "*** Subfunction: $subfunction ***\n"; + print STDERR $printline; + } + exit; +} #---------------------------------------------------------------------------- -# This section handles the request for Aleph version information. -# The Aleph REST API does not support this operation. +# This section handles the request for Aleph version information. +# The Aleph REST API does not support this operation. #---------------------------------------------------------------------------- if ($group eq 'ilsinstance') { - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); - my $timezone = $isdst ? 'Eastern Daylight Time' : 'Eastern Standard Time'; - my $tzcode = $isdst ? 'EDT' : 'EST'; + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + my $timezone = $isdst ? 'Eastern Daylight Time' : 'Eastern Standard Time'; + my $tzcode = $isdst ? 'EDT' : 'EST'; - my @aleph_info = `./get_aleph_info.csh`; - my $version = (split ',', $aleph_info[0])[2]; - my $aleph_version = (split ' ', $version)[1]; - my $currency = $aleph_info[3]; - chomp $currency; + my @aleph_info = `./get_aleph_info.csh`; + my $version = (split ',', $aleph_info[0])[2]; + my $aleph_version = (split ' ', $version)[1]; + my $currency = $aleph_info[3]; + chomp $currency; - my @t = localtime(time); + my @t = localtime(time); my $gmt_offset_in_hours = (timegm(@t) - timelocal(@t)) / 3600; - my $version_string = $version_xml; - $version_string =~ s/ALEPHVER/$aleph_version/; - $version_string =~ s/TIMEZONE/$timezone/; - $version_string =~ s/TZCODE/$tzcode/; - $version_string =~ s/TZGMT/$gmt_offset_in_hours/; - $version_string =~ s/CURRENCY/$currency/; + my $version_string = $version_xml; + $version_string =~ s/ALEPHVER/$aleph_version/; + $version_string =~ s/TIMEZONE/$timezone/; + $version_string =~ s/TZCODE/$tzcode/; + $version_string =~ s/TZGMT/$gmt_offset_in_hours/; + $version_string =~ s/CURRENCY/$currency/; - $printline = join '', $xml_prolog, $version_string; - } + $printline = join '', $xml_prolog, $version_string; +} else { + #---------------------------------------------------------------------- + # Instantiate a user agent for use in calling the REST API. + #---------------------------------------------------------------------- + my $ua = LWP::UserAgent->new; -else { - #---------------------------------------------------------------------- - # Instantiate a user agent for use in calling the REST API. - #---------------------------------------------------------------------- - my $ua = LWP::UserAgent->new; - - my $request_uri = $ENV{'REQUEST_URI'}; - my $response = ''; - my $aleph_id = ''; - if ($group eq 'patron' && $id_translation) { - if (!$sql_lookup) { - #--------------------------------------------------------------------- - # Incoming identifer requires translation. Since $sql_lookup - # is not set, convert it to an Aleph id via bor-by-key - # x-server function, - #--------------------------------------------------------------------- - my $info_prefix = "op=bor-by-key&bor_id=$patron_id"; - my $rest_url = join '', $x_base_url, $info_prefix; - print STDERR "*** Bor-by-key URL: $rest_url ***\n" if $debug; - $response = $ua->get($rest_url); - $aleph_id = &extract_alephid($response); - print STDERR "*** Aleph id: $aleph_id ***\n" if $debug; - print STDERR "*** Patron id: $patron_id ***\n" if $debug; - print STDERR "*** request_uri before: $request_uri ***\n" if $debug; - $request_uri =~ s/$patron_id/$aleph_id/ig; - print STDERR "*** request_uri after: $request_uri ***\n" if $debug; - } - else { - #--------------------------------------------------------------------- - # Incoming identifer requires translation. Since $sql_lookup - # is on, convert it to an Aleph id via SQL lookup. - #--------------------------------------------------------------------- - $aleph_id = `./sql_lookup.csh $patron_id`; - print STDERR "*** Aleph id from SQL: $aleph_id ***\n" if $debug; - $request_uri =~ s/$patron_id/$aleph_id/ig; - } - } + my $request_uri = $ENV{'REQUEST_URI'}; + my $response = ''; + my $aleph_id = ''; + if ($group eq 'patron' && $id_translation) { + if (!$sql_lookup) { + #--------------------------------------------------------------------- + # Incoming identifer requires translation. Since $sql_lookup + # is not set, convert it to an Aleph id via bor-by-key + # x-server function, + #--------------------------------------------------------------------- + my $info_prefix = "op=bor-by-key&bor_id=$patron_id"; + my $rest_url = join '', $x_base_url, $info_prefix; + print STDERR "*** Bor-by-key URL: $rest_url ***\n" if $debug; + $response = $ua->get($rest_url); + $aleph_id = &extract_alephid($response); + print STDERR "*** Aleph id: $aleph_id ***\n" if $debug; + print STDERR "*** Patron id: $patron_id ***\n" if $debug; + print STDERR "*** request_uri before: $request_uri ***\n" if $debug; + $request_uri =~ s/$patron_id/$aleph_id/ig; + print STDERR "*** request_uri after: $request_uri ***\n" if $debug; + } else { + #--------------------------------------------------------------------- + # Incoming identifer requires translation. Since $sql_lookup + # is on, convert it to an Aleph id via SQL lookup. + #--------------------------------------------------------------------- + $aleph_id = `./sql_lookup.csh $patron_id`; + print STDERR "*** Aleph id from SQL: $aleph_id ***\n" if $debug; + $request_uri =~ s/$patron_id/$aleph_id/ig; + } + } - if (grep /$method/, @allowed_methods) { - #----------------------------------- - # Optional local programming can be inserted here by uncommenting the - # switch structure below - #----------------------------------- - #switch ($category) { - # case ('patroninformation') { - # # local code here - # } - # case ('patronstatus') { - # # local code here - # } - # case ('circulationactions') { - # # local code here - # } - # case ('record') { - # # local code here - # } - # case ('items') { - # # local code here - # } - # } - #----------------------------------- - # Default passthrough. - #----------------------------------- - my $rest_url = join '', $r_base_url, $request_uri; - print STDERR "*** $category: $rest_url ***\n" if $debug; - print STDERR "*** Method: $method ***\n" if $debug; - my $request; - switch ($method) { - case ('get') { - $request = HTTP::Request->new(GET => $rest_url); - } - case ('post') { - read(STDIN, $putdata, $ENV{'CONTENT_LENGTH'}); - my $h = HTTP::Headers->new(Content_Type => 'text/xml'); - $request = HTTP::Request->new('POST', $rest_url, $h, $putdata); - } - case ('put') { - read(STDIN, $putdata, $ENV{'CONTENT_LENGTH'}); - my $h = HTTP::Headers->new(Content_Type => 'text/xml'); - $request = HTTP::Request->new('PUT', $rest_url, $h, $putdata); - } - case ('delete') { - $request = HTTP::Request->new(DELETE => $rest_url); - } - } - $response = $ua->request($request); - $printline = $response->content; - #------------------------------------------------------------ - # Remove the port number from any URLs in the XML. - #------------------------------------------------------------ - $printline =~ s/localhost:$rest_port/$ENV{'HTTP_HOST'}/go; - } - else { - #------------------------------------------------------------------ - # HTTP method is not supported. Return failure message - #------------------------------------------------------------------ - $printline = join '', $xml_prolog, "HTTP command $method is restricted or invalid"; - } - } + if (grep /$method/, @allowed_methods) { + #----------------------------------- + # Optional local programming can be inserted here by uncommenting the + # switch structure below + #----------------------------------- + # switch ($category) { + # case ('patroninformation') { + # # local code here + # } + # case ('patronstatus') { + # # local code here + # } + # case ('circulationactions') { + # # local code here + # } + # case ('record') { + # # local code here + # } + # case ('items') { + # # local code here + # } + # } + #----------------------------------- + # Default passthrough. + #----------------------------------- + my $rest_url = join '', $r_base_url, $request_uri; + print STDERR "*** $category: $rest_url ***\n" if $debug; + print STDERR "*** Method: $method ***\n" if $debug; + my $request; + switch ($method) { + case ('get') { + $request = HTTP::Request->new(GET => $rest_url); + } + case ('post') { + read(STDIN, $putdata, $ENV{'CONTENT_LENGTH'}); + my $h = HTTP::Headers->new(Content_Type => 'text/xml'); + $request = HTTP::Request->new('POST', $rest_url, $h, $putdata); + } + case ('put') { + read(STDIN, $putdata, $ENV{'CONTENT_LENGTH'}); + my $h = HTTP::Headers->new(Content_Type => 'text/xml'); + $request = HTTP::Request->new('PUT', $rest_url, $h, $putdata); + } + case ('delete') { + $request = HTTP::Request->new(DELETE => $rest_url); + } + } + $response = $ua->request($request); + $printline = $response->content; + #------------------------------------------------------------ + # Remove the port number from any URLs in the XML. + #------------------------------------------------------------ + $printline =~ s/localhost:$rest_port/$ENV{'HTTP_HOST'}/go; + } else { + #------------------------------------------------------------------ + # HTTP method is not supported. Return failure message + #------------------------------------------------------------------ + $printline = join '', $xml_prolog, "HTTP command $method is restricted or invalid"; + } +} #----------------------------------------------------------------------------------------- -# Return the content to the caller. -# The following 'if' statement is required to ameliorate the Aleph REST API's -# inexplicable practice of returning HTML in certain error conditions. +# Return the content to the caller. +# The following 'if' statement is required to ameliorate the Aleph REST API's +# inexplicable practice of returning HTML in certain error conditions. #----------------------------------------------------------------------------------------- print STDERR "*** printline: $printline ***\n" if $debug; -if (grep //, $printline) { print "$html_header" } -else { print "$xml_header" } +if (grep //, $printline) { + print "$html_header" +} else { + print "$xml_header" +} print $printline; exit; #------------------------- subroutines -------------------------- sub extract_alephid { - my $xml_ref = pop; - my @temp = split '<\/internal\-id>', (split '', $xml_ref->content)[1]; - return $temp[0]; - } + my $xml_ref = pop; + my @temp = split '<\/internal\-id>', (split '', $xml_ref->content)[1]; + return $temp[0]; +} + +# vim:textwidth=80:expandtab:tabstop=4:shiftwidth=4:fileencodings=utf8:spelllang=en diff --git a/get_aleph_info.template b/get_aleph_info.template index 5e5d056..446b88d 100644 --- a/get_aleph_info.template +++ b/get_aleph_info.template @@ -2,3 +2,5 @@ source /exlibris/aleph/VER/alephm/.cshrc >/dev/null ver echo $local_currency + +# vim:textwidth=80:expandtab:tabstop=4:shiftwidth=4:fileencodings=utf8:spelllang=en diff --git a/install_adapter.pl b/install_adapter.pl index 3b14182..c816695 100755 --- a/install_adapter.pl +++ b/install_adapter.pl @@ -23,17 +23,17 @@ $ver =~ s/u/a/i; #----------------------------------------------------- -# Prompt the installer for the institution's name. +# Prompt the installer for the institution's name. #----------------------------------------------------- my $instname = display(\"\nPlease enter the name of your institution (q=quit): "); #----------------------------------------------------------- -# Prompt the installer for Aleph's REST API port number. +# Prompt the installer for Aleph's REST API port number. #----------------------------------------------------------- my $jboss_port = display(\"\nPlease enter the JBOSS port number for Aleph's REST API (q=quit): "); #------------------------------------------------------------------- -# Prompt the installer for the license status of Aleph's X-server. +# Prompt the installer for the license status of Aleph's X-server. #------------------------------------------------------------------- my $xsl = display(\"\nIs the X-server licensed for use on this server ? "); if (grep /y/i, $xsl) {$xsl = 1} else {$xsl = 0} @@ -49,39 +49,38 @@ my $db_password; my $z308_prefix; if (!$xsl) { - #------------------------------------------------------------------- - # Prompt the installer for the Oracle user id of the ADM library, - # usually 50, and that user's Oracle password. - #------------------------------------------------------------------- - $db_user = display(\"\nPlease enter the Oracle user id for the ADM library (usually 50): "); + #------------------------------------------------------------------- + # Prompt the installer for the Oracle user id of the ADM library, + # usually 50, and that user's Oracle password. + #------------------------------------------------------------------- + $db_user = display(\"\nPlease enter the Oracle user id for the ADM library (usually 50): "); - $db_password = display(\"\nPlease enter the Oracle password for user id $db_user: "); + $db_password = display(\"\nPlease enter the Oracle password for user id $db_user: "); - $z308_prefix = display(\"\nPlease enter the two-number 'type' prefix from the Z308 Oracle table that corresponds with the identifiers that will be submitted for lookup: "); - } + $z308_prefix = display(\"\nPlease enter the two-number 'type' prefix from the Z308 Oracle table that corresponds with the identifiers that will be submitted for lookup: "); +} #------------------------------------------------------------- -# Prompt the installer for a patron's Aleph id to be used -# in testing access to the X-server and REST API. +# Prompt the installer for a patron's Aleph id to be used +# in testing access to the X-server and REST API. #------------------------------------------------------------- my $aleph_id; if ($xsl) { - $aleph_id = display(\"\nPlease enter a patron's Aleph id that can be used to test access to the X-server and the REST API: "); - } -else { - $aleph_id = display(\"\nPlease enter a patron's Aleph id that can be used to test access to the REST API: "); - } + $aleph_id = display(\"\nPlease enter a patron's Aleph id that can be used to test access to the X-server and the REST API: "); +} else { + $aleph_id = display(\"\nPlease enter a patron's Aleph id that can be used to test access to the REST API: "); +} #------------------------------------------------------------------- # Prompt the installer for the IP addresses of the remote servers # that will call the adapter. These IP addresses will be inserted -# into the adapter's whitelist. +# into the adapter's whitelist. #------------------------------------------------------------------- my $ip_string = display(\"\nPlease enter the IP addresses of the remote servers that will call the adapter. Separate multiple addresses with commas e.g. 10.10.10.10,11.11.11.11 etc.\n"); my @ip_addresses = split /,/, $ip_string; foreach (@ip_addresses) { - $_ = join '', "'", $_, "'" - } + $_ = join '', "'", $_, "'" +} $ip_string = join ',', @ip_addresses; #---------------------------------------------------------------------- @@ -91,27 +90,27 @@ open(FH1,"<$input_file1") or die "Unable to open input file $input_file1\n"; open(FH2,">$output_file1") or die "Unable to open output file $output_file1\n"; while () { - if (grep /source/, $_) { - $_ =~ s/VER/$ver/g; - } - print FH2; - } + if (grep /source/, $_) { + $_ =~ s/VER/$ver/g; + } + print FH2; +} `chmod +x $output_file1`; close(FH1); close(FH2); #---------------------------------------------------------- # Generate the api_adapter.cgi script from a template. -# Substitute the IP addresses into the whitelist. +# Substitute the IP addresses into the whitelist. #---------------------------------------------------------- open(FH1,"<$input_file2") or die "Unable to open input file $input_file2\n"; open(FH2,">$output_file2") or die "Unable to open output file $output_file2\n"; while () { - if (grep /WHITELIST/, $_) { $_ =~ s/WHITELIST/$ip_string/g } - if (grep /PORT/, $_) { $_ =~ s/PORT/$jboss_port/og } - if (grep /INSTNAME/, $_) { $_ =~ s/INSTNAME/$instname/og } - print FH2; - } + if (grep /WHITELIST/, $_) { $_ =~ s/WHITELIST/$ip_string/g } + if (grep /PORT/, $_) { $_ =~ s/PORT/$jboss_port/og } + if (grep /INSTNAME/, $_) { $_ =~ s/INSTNAME/$instname/og } + print FH2; +} `chmod +x $output_file2`; close(FH1); close(FH2); @@ -120,32 +119,32 @@ # Optionally, generate the SQL lookup scripts from templates. #--------------------------------------------------------------- if (!$xsl) { - #---------------------------------------------------------------------------- - # Generate the sql_lookup.csh script from a template. - # Substitute the version token into the path of the 'source' command. - #---------------------------------------------------------------------------- - open(FH1,"<$input_file3") or die "Unable to open input file $input_file3\n"; - open(FH2,">$output_file3") or die "Unable to open output file $output_file3\n"; - while () { - if (grep /VER/, $_) { $_ =~ s/VER/$ver/g } - } - close(FH1); - close(FH2); - - #--------------------------------------------------------- - # Generate the sql_lookup.cgi script from a template. - # Substitute Oracle user id and password, Z308 prefix. - #--------------------------------------------------------- - open(FH1,"<$input_file4") or die "Unable to open input file $input_file4\n"; - open(FH2,">$output_file4") or die "Unable to open output file $output_file4\n"; - while () { - if (grep /DBUSER/, $_) { $_ =~ s/DBUSER/$db_user/g } - if (grep /DBPASSWORD/, $_) { $_ =~ s/DBPASSWORD/$db_password/g } - if (grep /Z308PRE/, $_) { $_ =~ s/Z308PRE/$z308_prefix/g } - } - close(FH1); - close(FH2); - } + #---------------------------------------------------------------------------- + # Generate the sql_lookup.csh script from a template. + # Substitute the version token into the path of the 'source' command. + #---------------------------------------------------------------------------- + open(FH1,"<$input_file3") or die "Unable to open input file $input_file3\n"; + open(FH2,">$output_file3") or die "Unable to open output file $output_file3\n"; + while () { + if (grep /VER/, $_) { $_ =~ s/VER/$ver/g } + } + close(FH1); + close(FH2); + + #--------------------------------------------------------- + # Generate the sql_lookup.cgi script from a template. + # Substitute Oracle user id and password, Z308 prefix. + #--------------------------------------------------------- + open(FH1,"<$input_file4") or die "Unable to open input file $input_file4\n"; + open(FH2,">$output_file4") or die "Unable to open output file $output_file4\n"; + while () { + if (grep /DBUSER/, $_) { $_ =~ s/DBUSER/$db_user/g } + if (grep /DBPASSWORD/, $_) { $_ =~ s/DBPASSWORD/$db_password/g } + if (grep /Z308PRE/, $_) { $_ =~ s/Z308PRE/$z308_prefix/g } + } + close(FH1); + close(FH2); +} #---------------------------- # Validate the installation @@ -157,14 +156,15 @@ #------------------ Subroutines ------------------ sub display { - my $text_ref = pop; - my $input = ''; - while (lc $input ne 'y' && lc $input ne 'n' && lc $input ne 's' && $input eq '') { - print STDOUT "$$text_ref"; - $input = ; - chomp $input; - } - if ($input eq 'q') { exit } - return $input; -} - + my $text_ref = pop; + my $input = ''; + while (lc $input ne 'y' && lc $input ne 'n' && lc $input ne 's' && $input eq '') { + print STDOUT "$$text_ref"; + $input = ; + chomp $input; + } + if ($input eq 'q') { exit } + return $input; +} + +# vim:textwidth=80:expandtab:tabstop=4:shiftwidth=4:fileencodings=utf8:spelllang=en diff --git a/sql_lookup.cgi.template b/sql_lookup.cgi.template index aec8f1b..2539c66 100755 --- a/sql_lookup.cgi.template +++ b/sql_lookup.cgi.template @@ -9,11 +9,11 @@ my $aleph_id; my $debug = 0; my $patron_id = uc $ARGV[0]; if (!$patron_id) { - print "NULL"; - exit; - } + print "NULL"; + exit; +} #-------------------------- -# Set variables +# Set variables #-------------------------- my $db_user = 'DBUSER'; my $db_password = 'DBPASSWORD'; @@ -25,15 +25,15 @@ my $oracle_sid = $ENV{'ORACLE_SID'}; # Open connection to Oracle #------------------------------------- my $dbh = DBI->connect ("dbi:Oracle:host=$oracle_host;sid=$oracle_sid", $db_user, $db_password, - {RaiseError=> 1, AutoCommit=> 0 }) or warn "Unable to connect: $DBI::errstr"; + {RaiseError=> 1, AutoCommit=> 0 }) or warn "Unable to connect: $DBI::errstr"; #--------------------------------------------- -# Prepare and execute SQL statement +# Prepare and execute SQL statement #--------------------------------------------- my $search_term = join '', $z308_prefix, $patron_id, '%'; -my @sqlsearch = qq{ select /*+ DYNAMIC_SAMPLING(2) ALL_ROWS */ /*+ ordered */ z308_id - from z308 - where z308_rec_key like \'$search_term\' }; +my @sqlsearch = qq{ SELECT /*+ DYNAMIC_SAMPLING(2) ALL_ROWS */ /*+ ordered */ z308_id + FROM z308 + WHERE z308_rec_key LIKE \'$search_term\' }; print STDERR "@sqlsearch\n" if $debug; my $sth = $dbh->prepare(@sqlsearch); $sth->execute or warn "SQL execution failure: $DBI::errstr"; @@ -48,3 +48,5 @@ print "$aleph_id"; # Disconnect from Oracle #------------------------------------- $dbh->disconnect; + +# vim:textwidth=80:expandtab:tabstop=4:shiftwidth=4:fileencodings=utf8:spelllang=en diff --git a/sql_lookup.csh.template b/sql_lookup.csh.template index ec2aabf..a314123 100755 --- a/sql_lookup.csh.template +++ b/sql_lookup.csh.template @@ -2,3 +2,5 @@ source /exlibris/aleph/VER/alephm/.cshrc >/dev/null set parm1 = $1 perl sql_lookup.cgi $parm1 + +# vim:textwidth=80:expandtab:tabstop=4:shiftwidth=4:fileencodings=utf8:spelllang=en diff --git a/validate.pl b/validate.pl index a823b05..df0d5d9 100755 --- a/validate.pl +++ b/validate.pl @@ -1,13 +1,13 @@ #!/usr/bin/perl #---------------------------------------------------------------------------------- -# The following BEGIN block is required to trap error messages from -# 'use' statements. +# The following BEGIN block is required to trap error messages from +# 'use' statements. #---------------------------------------------------------------------------------- use strict; use warnings; my %perl_modules; -my $aleph_id = $ARGV[0]; -my $jboss_port = $ARGV[1]; +my $aleph_id = $ARGV[0]; +my $jboss_port = $ARGV[1]; die 'No correct Aleph ID was specified.' unless (defined($aleph_id) @@ -18,39 +18,38 @@ and $jboss_port > 0 and $jboss_port < 65536); BEGIN { - #------------------------------------------------ - # Read the file created by install_adapter.pl. - #------------------------------------------------ - my $textfile = 'sql_lookup.txt'; - open(FH1,"<$textfile") or die "Unable to open $textfile\n"; - my $xserver_licensed = ; - close(FH1); - chomp $xserver_licensed; + #------------------------------------------------ + # Read the file created by install_adapter.pl. + #------------------------------------------------ + my $textfile = 'sql_lookup.txt'; + open(FH1,"<$textfile") or die "Unable to open $textfile\n"; + my $xserver_licensed = ; + close(FH1); + chomp $xserver_licensed; - my @modules = ('HTTP::Request','LWP::UserAgent','Switch','POSIX'); - if (!$xserver_licensed) { - push @modules, 'DBI'; - push @modules, 'DBD::Oracle'; - } - %perl_modules = (); - foreach (@modules) { - my $module = $_; - if (!eval "require $module; 1") { - $perl_modules{$module} = '[Module not found]'; - } - else { - $perl_modules{$module} = ' [OK]'; - } - } - } # End BEGIN block + my @modules = ('HTTP::Request','LWP::UserAgent','Switch','POSIX'); + if (!$xserver_licensed) { + push @modules, 'DBI'; + push @modules, 'DBD::Oracle'; + } + %perl_modules = (); + foreach (@modules) { + my $module = $_; + if (!eval "require $module; 1") { + $perl_modules{$module} = '[Module not found]'; + } else { + $perl_modules{$module} = ' [OK]'; + } + } +} # End BEGIN block #----------------------------------------------- -# Display status of Perl modules +# Display status of Perl modules #----------------------------------------------- print "\nStatus of required Perl modules:\n"; foreach my $key (sort keys %perl_modules) { - print "\n\t$key\n\t$perl_modules{$key}\n"; - } + print "\n\t$key\n\t$perl_modules{$key}\n"; +} print "\n"; #----------------------------------------------- @@ -60,39 +59,36 @@ BEGIN my $ua = LWP::UserAgent->new; my $operation = 'find-doc&doc_number='; my $x_url = "http://localhost/X?op=bor_by_key&bor_id=$aleph_id"; -print "Sending $x_url\n"; +print "Sending $x_url\n"; my $response = $ua->get($x_url); if (not $response->is_success or grep //, $response->content) { - print "X-server retrieval status:\n\t[failed]\n"; - } -else { - print "X-server retrieval status:\n\t[OK]\n"; - } + print "X-server retrieval status:\n\t[failed]\n"; +} else { + print "X-server retrieval status:\n\t[OK]\n"; +} #--------------------------------------------------- # Construct a url for the Aleph REST API that # will retrieve patron information in xml format. #--------------------------------------------------- my $rest_url = "http://localhost:PORT/rest-dlf/patron/$aleph_id/patronInformation/address"; $rest_url =~ s/PORT/$jboss_port/g; -print "\nSending $rest_url\n"; +print "\nSending $rest_url\n"; $response = $ua->get($rest_url); if (not $response->is_success or !grep /0000<\/reply-code>/, $response->content) { - print "REST API retrieval status:\n\t[failed]\n"; - } -else { - print "REST API retrieval status:\n\t[OK]\n"; - } + print "REST API retrieval status:\n\t[failed]\n"; +} else { + print "REST API retrieval status:\n\t[OK]\n"; +} #--------------------------------------------------- # Test the retrieval of Aleph version information. #--------------------------------------------------- my @aleph_info = `./get_aleph_info.csh`; if (grep /aleph/i, @aleph_info) { - print "\nAleph version retrieval status:\n\t[OK]\n"; - } -else { - print "\nAleph version retrieval status:\n\t[failed]\n"; - } + print "\nAleph version retrieval status:\n\t[OK]\n"; +} else { + print "\nAleph version retrieval status:\n\t[failed]\n"; +} #------------------------------------------------ # Generate a URL for testing the installation. @@ -103,3 +99,5 @@ BEGIN my $test_url = "https://$ENV{WWW_HOST}/rest-dlf/patron/$aleph_id/patronInformation/address"; print "\nPut this URL in a browser to test the adapter after all the installation steps are completed.\n\t$test_url\n\n"; exit; + +# vim:textwidth=80:expandtab:tabstop=4:shiftwidth=4:fileencodings=utf8:spelllang=en From f103501908d179608d4c8df1cc36b1ff28249bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Sun, 8 Mar 2015 14:04:49 +0100 Subject: [PATCH 09/31] Correctly formated documentation in one place. Keep the documentation only in correct Markdown formatted README.md file. The hardly editable, readable and versionable MS Office .docx file was removed. --- Aleph_API_Adapter_Users_Guide_v1.6.docx | Bin 37415 -> 0 bytes README.md | 568 +++++++++--------------- 2 files changed, 218 insertions(+), 350 deletions(-) delete mode 100644 Aleph_API_Adapter_Users_Guide_v1.6.docx diff --git a/Aleph_API_Adapter_Users_Guide_v1.6.docx b/Aleph_API_Adapter_Users_Guide_v1.6.docx deleted file mode 100644 index 83d1197cbc34f7843f312330f5422a7a23941bbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37415 zcmeEtQ?F=2nCw2=wr$(CZQHhO+cwX(ZQHhOd(PaOo6ODpgqeO=>!H77rB|v_-CtG9 zO925R1Aqd60{{RJ07&Eo9aR1S07wG`06+o&2hvvoGHb=Fh%us3nip>?;h#xDd0 zB+mx`{8#_K>P$xi0(;cEee} z+e5XEnyuQr#oi7cKbG^(zE?BCW@XlBCT<=`H>|>dh~bVyd5Bixp1pmlCmdSrU1EJF z9|@D+mf_ujKFf+ge`pz3qK-IRt5~QTsH7eYo~w2dpH1j11BH?DQ-X?LD^@hC2qw2j zQ6?%RJ+hmxN+q?voylh>fvAqWe(h>>r~-*n#G;9iG-0Oqy`OlC4OwITy0;+QWeiQ= zg_D`$hjn$)v<;>j+gg9W4GQ-rEC=RcOgrR7W| zi_v>}dpJi`yyweF1rv}}3V(lLoh{omA0!Og0sV=-LH#u`O~CNIe&ZK1FM$VD7{b4y z-uFtjc9CoTYi#bJwwnLG0;dNZ<`JZ8O#^Egw(TGO$UA)GLU$t63g+esl`rk{|jA|gd2>H>y5v8`p&a9f!%n7FyF!jx9}8dbW3KUl=FDSQ83FT zhC5((3O$`(n}2bidTnR&fCT{X`wIjh|Nn6uHvzNt@gHzy|4|s~AIJ3^O{|^hX#ey1 zzuf*GOz8iz^ytJ*Ngx5}&|8pQgE1erO-RCp^&Rm7H^NJx;Ebz3Zm?6u8$I1c_#N__ zx+hPklX5FrL0L;&W!#{(1{%xxYgzhYyH@Tttx;g|8e>|Sfo3pbGj$fWb}Mw;3CRWA z(F~P^2aTZu#Jq&2Om#)tA!9wt9r4%_5K3_gQ&`W@l^s2!PijD!8KoNZ3I&7G}kfyh5lMyImU&+ z001@s_#ZbrM`ODGOB7=}BNv;0lK7um`2R@r=bs?{tNx!os*)Mx2I=8L+=zdKj&j&3 z<_MRHq3~To2-S5?v(<-OjrIJ0ykHAMm1zvd-De-Vq5$NLZYwn>r{MwTfBoc@pT|mYz2jNtjE+t*0vmRjX^|%*I4w= zg*ohi70;!1z~4zF_Zo>aocON#AA)k{JI2!W(2jc7S&qK^?8=f;?N5!Z`VJtVVuwJM zk_7hzafwrCoel>x?ati#;`*GS zab)2@L~eY3pnT>HrHTh5_T2|+4`eu z!veHYSX~5Cv)Q7Z&+buXoE#in(rz)kGg2_2UNkWYW^p*r`8#?{=&xM2H|*i>LD6bm zBR{NRg;^;?@7o2Kye(t7E?f5R7W1Q!w-x-zFxyOQ;rqe;xtQJ?sgshKO-@P(A zceiR1J*4hktkL}p^|?-Kfu|&<9{l^TFA?r$s7beXy4oR#Iy&4DU4Ir~gNL&?(oOXj zo3(=??FFTI(%jGh?;8(gzRVBZmSYcndz8*ufZ?MMrP@Yp(Y#-C^gAc~H`#p4o_bxX zjc9-HZbGd=ZmGFm3N=ROZ+fq3N3kMYT5>~_uOEWgks@mjBERNVALw3_TfzZteUoO? zB5oI}aY4Ft!tG5lx1(#0!JYNdF#5>tPXH><8a$5R1TP%=sL%3WLBH7SQzrVXpXtwT z&FePTtq$&uq_mo@04`X?j>Rr^UY?m7?@}qX)oIbkz;5d&4%6EX89hL_USG$cS1&y7 zv^G_r+xVOBX0qDXzQV3_y_k2nLtErh2weN5TsZcx`KV9Zvjsui*+JpP64)5S)kG4PS*-zjLHBG<|t>j%9$BfVqzyxD~rkG&Y& zoX1(boW~w6KHl3GZr7Az3nHM!#c8BL^ICsC?Za4!B&dE_BLy`xHyRN zD}P=g)n&^xv{5xQXtp(xkjSS|43r3|tLbcXRtX9W^5zsq{968qU>bZ4MGa_olU*hE zG0dLSAkOE)+SW_4)0h=b_*SR6iO;!>I2XQKDd+stv>4AzOB}5Uj=93P+FR~xyxs@T zox7c=oI;VRTVPIk|EL~f;62RvR;Oa0(FrwZ0qVJnVpqTws*IQcwSS-2Lr7yCP|KZ7 z%&UhaJ;n;mA?4Fc1W>CnFv`sS8Ma|Q7YaY9`l#5ec188V=qI$d-KKZ&?I1bxv`x$< z>-qG(ox$^rc=WnmZ;uG2RX&x*%luG)!z_AdPm4yRWGOI~-XpGgD#Rxgoz_2%T@2?d z?@Nc^5BxZ4g`q$WLETH@H?=coD*(;`Z}OMr)o`|Mp%2>1Km0}Vjy9mCs7@VpQP zIz`mPPaX{R)8qakdHQ92X#-H*1$CZ~MfD3n|HW>EIm9~M!x#=xRG-L@;Av1OP5LE4lFztQ*KftT+7KXDDd}r5E8oGyM+#c0vwNm(`RB z{p}-qSGv^L>30HzaXA}|{oo$~@0L$2_k7fhr`a}Bjq+srG@+A7LDPNn$up8e_lYes z?_+R>xPhcD+lCALIWEF7f;g~qqa&*N+41r}o{k9T5tBHWI;GMN%~evulsCs$WEh}l|`;c-!jQ@6r?+h?Pmf$@q%;1?aq zV)jg@c{(GrK<@2h8$rQyAp*s9K<2SztC^vD9}EPta{dzVT?P(D)-T@i4V1bWf6ZlK z>_^b-kF=l*dI%Ojd%hvgT_**cisuxQ3mFT{HADedWShcuy-@qgP zvAIOU6p%1D3BLn0NF~D537hxD4oPv>ZVUZI!qBu5TYv#A&0-A^qchc zhl};|*bI!Rs%~3nO(dp8q7F7mW&VhT2KuRNJYilOXj~TE*@ctFRv@JfThrjVY%iUh;X7E)Pzh+pguFay|G@`rXQ1gL$iCrIop`+*?XQ{~kas zO~5jw48wBLss?-i#50NoIXQRbg%_w3XC+?(MIn&}P#|Id^e31?KHaNlzC&(W!`bgp z5|m)(<1|u_1kstRBT%Urop1eNjYbm-{nY?`Mp(Z+vqX46-T3^{<=7sKH0W$_!zL#VOjK>$YBeQxs|T~m+DA*06hEBL_Zup2W_)( zd6zw&&iYg0?lrs;SnN$+2(H@OEwA&&(-2rjjG4TLa8KZUSFY<4`Q)A^EFvVXJCh2%KHzUeoMh4D`J2gM0O zyZ~FqK7TmI5WN@$8OG5ay08l*qlnw}FJm)R6bQvtlBm$~%8616Vm~ z&;&u#PUE<`$+7YA()n}T`P1gG-o|d@1OsTh864R|-4FOT|E8qi&pv&a4-_>s#Xl8? z6Mj47x(>B_lcf8*L~$N!hR#x~SE#N(5eYJl4Lac&x;>FUXpaaLG=a$dflWNViHPpA z=l)7Req6@*9UX9R85J_Y1>uXxW~N~09IEm5H9ds=^0#1=GZ{1Iow3`dYzaV4 zk^x%b{T7-99t&NoxL#En=}yS+?DvX6Re)1t&8t8Yso03i;z|)LIOzA)FGnk#2=+qS z5YLe8B*6{6bnJr^OZDiloV65q1K3gn8HJaPsnB2`&QWAVq&=ZwMyQMIaf==BhWy({ zE>fI=^s?B4FddBURsMz>cpHC6JhiPqLtqIdf<8N_ao7!;dJ}9WF#ossKG6IZuE(hg z0=X3UDUDLaXo{#ai)t>Bow|#=FO50N%1(gTMKv~iJ;coF+dNXz7t)f($lYGz<3Yjd zYrkAhpU?QJ1H^2s)pXJ&Xe9=@ST+i(f?Wc+(Ntu1Ic%dXgs{k&tIM{ie(8{tUy(F8 z?^$6{EhJs$_M}mkd@ooEvW(zdY-skEA`=n<2yDDGjF341v!LWo(XN*>dHB4Uqb`gF zctl{^pKKuyV`T*OpU2zZF)afV5D&6&0Gx7_c8m8EjxB94Efcua8j^(QAD_}WRW0^4 zHvr+;IZ#yzS{+qXdK}B5q4otJ5Beep$r`CdwAnhbbSC8adSF!2t{mg+Jwhg7Q1Inb z&j?BZ%BZ&BmQ}s{!!1s2t&a5V_Llf=1qM8Xa8?J;INLV>Gl24WiFx;sdCP<~8l7I( ziVU-w(^5okA~JqR6<8h)L^`+op}omgEQ$nzki6M;JU(LXed2(k_&Ok5etTQIYK^d> zKB;L@7Zs^zg5^E#Ay1h>oB_xlUAA}9ySxVrz$ZT!cpVHPT(tNr&jN9r1*?$=Hgqr% zW`#fL3YzdXks-c=!P>~kR@fg^71d1WVLXsb9ZQ+4c2vvxIXX+?K|#*Sl<`$&>#Aii zx7X%K?k6 zGS`Wm3IRZGk3j*Z0?a(#GsYA^mTx&kFm~6DkT$WT z=7Iz!@)`NO2#F4OqQt$~*9@zn3bE6DUc~hbtYh+cnI2hX=^L0SO#u)y<{_9-WR1Jt z6dKIRv|{J^x-r;H_WZ#vRCfP0pq4^}&YVTJOi^K1Ho1F%%B!SM1V@du=Eyv{euq+#lM`IiDWVHR227oik5G(j|4>^ukD3bmwfFx?|i2x$^!uY1J?%ENA8 zfgQ|$JwyXwuv7fIo+t*{A!G6LB6knqBeU~QUoMkc zSA!=F)pF?q9uKM(nBGNLiL!{e5S0BBo5Bh0@Oy8`SrHJfT4`7waz0?B+?z!=r8I9d z`PzPWn_e5rTNv78MrC zk!Q=?<~h;Im}A%VJ`A`INN9QaJ{eR)4j)ObU=!&cy@|(tULVD{z0K=)a_oSL(8x;y zQ!##%%xm$#qgtC?BQ%?yRjHafEI=^Rn~eAvI)56%3ix?Zm1UQ zcb=8Ad|nP=fuV&C{wyMXL{cKL>b7-`)p*)XH1drqo!kb4qRBJ(gqx#?!7FlU2L6ed z#@D0qn^fR~4C+eib0|Q-^JsK`l6^duas{fSsu%um6VlS-(q<@?tw}=F6K*lYDMN}_ki?n3^6?ID0_U%#lTa*g0>JdreBw(>sNZB7= zm8WtcASIlM=?JwXiJCxjAOnhhhHP=9emSHPBg4ptH3k{zE2dr9F+V|61E(df4R4|3 zbEGbemrGb=f_*8I+8jf*aml)%e)x7)$YPPpp(M(NS?yUGehr+CF4^nw$5koZEp3!6 zrjn-u+=`w$j>i3*9X2Y}XMw%PT>8XQ)o7iA7gtw1lige?q{%JWKEZKnQ;<79fB?dr zX_#X1UShOd1woj@t9w?)+pOn-CJ?g<6#+bcy zhW*vtGg4+EHA7^xlDHiOjR6^=tO0%VBt^B}Y9oJ{r>PPAIbsqDEYAR8!{RzJ(lN@s zJQ6&k5$z&&46*~1nmuwR(!A^I>fpD*5FjRTum|h4!amB4gRz~&9SReh=dOiTVG6cCt#1e1z zJDzeES%^-`Nt{d1)>oJl*fO}s%wXSON$eX7@kznC!{t=3s@tZBYiLg$(`mzl*IoEU z#R`~f`??5HK}eROevCJ2n;xS8hd=xXo=sv$A_p>g)@rr_M`ff+9hPPp+H=5zqYpfL!6~9^0kv?dUN||tD+x7FPxCs=CifkqFS!!Fr31d78a{40fwkxIf zxVjj&|Exh$L#Bva)M7%yW|xE0@`j77tjr~KR(vIV3~3aJ-$B|RV}LW@7spr#6=Kxe zBGLQ9+t;G7MVdxF33siHEn(nJ$a(*z=hy_RjvL%+YaAZ1u}<1A+aRN@IQ_1+c71+@ zoMWhC_{%%x{0RASVfQv?RNRoLjEM?F@n>7=h%ep5clMX66vJMr>?(kefF*R7adP%b&S^h-uSG$))bb6gG8YEalPvd1lF zW_$3&Ll=*7kRj)iHCPd`n5gDo!a5D)9(;mkYUWH7Ic@nML;-7+gdA2;XA##DcS5O? z(2G~8S$Y#9Ltwa-4$mV5*nJ}sAw}17BG6KK1E2ZW$hu}~$VmkXob{H;Ejf$JE9qiT z;ckA?38n#U*&703fIl&J@)7}c)QW-qr3~C@W+0PK-+E-YcI06UNg4PdntdCZKcUn3 zgg3Syil$T^$2gNl6Ino*{ta{_Hp7sDJamQvi$(vgAB$ zz~NE0MGlJX+3HD`rb|6G8~z?oCIpMr*BGLNr3p6@Q+v`X@D*L&)FW{?k@O6= zL8mVgbN6PJV6h!lz|CKOmgsP|3dd|1N8kx$4(XKRwMMHaizYZ|P9()BqDB_anUfk) z3+u23o9!Kx`FJVi?q;>qja{>WQUgO~&5Wu7MSjeyRQDpv6ZBcj7ml2>vNIM$Xmg8= zQPO@uW&KW2CkWe{oYaD0Q=$!A%I~2_J|iHUlxpK*}_Uy_x% zIJHwss3L)eXctFH&5-r4YFr;^5|P01m7bqn-QDR0J?#ohaFXDBT?&2J9T!3GD$qgZ zBSVfaUKgUr9aEWq`#GW(j69kn#?$ znpsH!+8!BFN{)@iVzK+?ncFtA-l~L>?B0#STLcJcwRGWFwZb%*O!|v>gD$a0o%+;U zGft;HbEX$psU9_x@KNEyu78s<(chFlGq$!_XLZI&E>3gS^!K*x#fzD1TGn(l0Zl2` zPCr*z|2C|fw@6_NuJ32EW$T_$N^uR($fmu7dd@S@mzo0)BrE4d@g>mO0B#3nJ|Mmx zSK;WL&ZjxGGAZfNKE?LLuBpSayAqjsv7BkcPox8AxM@`g%r-X;wif?;9X4CeU8+W> z7K{TvH*QQ)ai{~N{gpv*m2jFQ6i`{Qc=0JCU|P5!imZ` zlAhS5-zsZUsri*hDSfY5a4|=y1uQ*g?I}c6*w3*Mk1D^5Ra+)p89h$z7s+wxnr@~Z zk#uS(I$eY>O@w5dazoq7v@f}n&bx>%SV@xS3K?bk=56ZVBE^S6&d-td^pm@_ zxW-Z!h8{1Wib%yIUl|=h(@?Q}f5^k-(VZiLs*eXeQNnCUk4hWuk(#tnrcpwsIUNzG z^gdgvO~K3|r0BZ@L8#puSCKp~r_&rQTSZf$n%kEMXosq01Q&i>BYdL$oLX;Nr2fOy zJ8PH{hp||L#LHp7S5yg*!R%=wsJAdsd`@@yqy?P5qEfw0Q~%CyCc3#p2^7qB6LX{w zzj77lrp`-$uI?&rNZ~9`We%>HGp__@P+mr)JQ=e3=&ic+emU~FxfYkPVK{Pe{E(m$M#98w zO0#vve`-GzG?6Wg1Ab8~{T&O5>)MbRTEpixC@1=apSG)n?*~%vSD^OnBA2PQ+66rc z`L5z=$egYk%f#SiNL^?qYd^y8f3A8N)y1mjqY|~1-_ASg8|i^Gw9M<}V88bz{4F9v zfn&vx%pOMz11fxoveH0STv|e&RW$mZf`9j6cJ@cYWclny{Da<30^mJ`rQp|j@&jWn zZ-7|PSyra8!Aa);MWV$PC?#8yVD&v*4ZD~Wq6?N;7^^wrUFc9$Bji$ z>8ciFcID@oq4HZ4gtzj?GzfyU4fv|T2o6(|euvM&-0pnRij#m^SpSDDA%lOfH`gSea{0U@rETV+xwy_2Qh!2zp)ORdm1|XdW zjgu}&&svjF7WU{oNqy^`A42$M?3a@IAZRIwC%RPKsrrTU$|=JVXwGz{gHC~3>z<~`#UG8;DZMmrSatw6P-E2h+HQy3N-ZTSy)q3#4YKcMzX?P55IwQyb=n7j zPiQg);UY0S4hdU( zxkqe!*a^pant9q0mbSiYbSpiRXWR&fCE*u~>izrT^(Y@2jUa=lNVYi!@=RIQs^70+ z1MM|9$ouTAFa!g$vEms}?`a6cl!#2;>1=%J(bmtt7+H9solzTDxQw7PIRn}nC~{)| zq^E75E4}tw{P4xgC$G#_KepV+8Pht^E@83qF0mktN9n9fy^kHHp~dONfe{G29NPS1 zSqpT_;!e%X4DYZl9X$*rjCk7f;RuISXl+|T>2k2iN`9Z6FPwT$mgK3(6p-rZ+S(1r zW600Kma-vZ?urHlT-s_8AjO{W681MN*eXMT;s>MP^{UVn>?8GA(c#Ci11pNws~BxH zm_lk##QKWL`thadit($Rac&GZ07G|$%a%3n_4$;3COn!^0mlYYV4|`9nCls=$7+t> z&o`bxa4a=5B?Jt$#9;1CBzc+`)y@=xE55h>&Jiano11E#4Q?cUQ^xm6zVy7HCfW7P zFfoG(&ryzRx>by38miBy3Y-9F2^@B2zj%vSI%vpYD;8Ek;f$|IEU_Jdl^`G&tM8XI zC4quT%_PC?=Oj6-McIxy-qKw!hDHvSVVuUL6(4erJ&*Z~{@I*)J@ zO}H-XuO}YKc%z$NeA`1p(Cy$>+jwUDvaBH33{B>)o}Zhqv3Rs=?LIoGFM{+bGu){G zd*i~}wbc(ik5UubgWn!s^!@`$^RoJV@rZ=g^IU6j3fhtGB3Sz^%02pifVIu+et zC%g=vyeq0Yr-0?&-Q2zh9`tZ+=IEDK2byl8Ylv1 z_iT!Nh;oCfVPASmCcQrd85=Llw{#OpX}`rdcu|L;m4lxP>`To+?2sdaa~!2hOovL1 z`(zr#t1if@ysEjH1b=({D)owM{RS}GUZWHe&VMj-A zs_QtO4vDe?6I<`ga^c3?qqf>->lzv4SIJ{QMicB=LxXMC;avs4FrYLc=gXKB@!qtobX&Xl}#=0-_Bn@YE!IGNW>t5_L$(tdi%kg zY`wgn9$f#9=K>%jjx19B7j#;(@KX(xT=V+!XWh*;CQ7j)Ohm$5DKTq|<{*0w=O$f35%1p%StWgE{Eq3B7`)L$k3{JdH5`JwKF(q-$Oc6R|uM2zu&rM=6rvU zUPZ8c*Uy9U6025>ztG7>E?BaTN^@vgdG^T?Dy$ClJ%Zpl*Sx8PUrHG**7kB8VdRaD zATPd@Qn_S0jE0Y*CS70O(o<_Vi#w|)C#SBfRuV7bn50qDkfl*)(%YcM!vjZ|*v{X1 z6>Cr}o@nlc3rk(@uRrEFECr7bP$Cy^>s%svnIL5zS5p^V=Kef)C8kzV^!SMqWFTtX#q&@#z2+@)V@@U>07zrc={Zn_#_l zU{zO`*g5R-wCQaJ#1;E3zh_`8(~M}M0wPgjgyFdN`m$q-CpfutZ}Vd@F%lTwztZ0! zdh=qBNfe2c4C}l*W6QO$RfVl}+2T4*!gs%1b-1p)eMA$Xb|%H#k9o+l&S;1#LQ(6} z4^M-LnODiRD3*=Y}Sm3a;YiUNq?)@6^=!7 z_dvv(6pr1);y!%;GwnwXw4uw!2yH)gyp>iYvXz9|QD3)5ROlYSc zH9rj#E%WaXa7w4pO0HHis|_rOx&?R}z|(T<;km{hV|-0!j}uHohtbL#7yL8DDi+zb zNeZVN*3&1dMu=S6T^kAJyP@MW0vuG`lrSa=l)X~0k05X9!JvL8sR5}jVpR68U4ITw zKH&8J8J8#PyUf4%80yE{KHrXy_kLWzR>KM2D!a!IN*{lF8gU-uFv8#Hk0Q;d+3L(p z>!D0BtZ{nB41Rh=)vBJhwY6H#qgBWn*rs@4e=hB?lftHIuhektE02{_G;EasQL@*Q z+A-EH?B-)E)ct-dJ0AIJUj4rEKsjm{x-Csyeq(A(5H>Tp^$RJnFj_&YPRjZy*UP9l zVkS_xmrnXbB)_$QMi6`A5(Nxa8Nhi#Q;cI{VG#NYkE)6}+Xb%LztKiNX(y(p^(D`d zd(KW)5Z5(3we070>VAIcWcdt>fQ*kBoBR6I>8F@3vg{}9qboSmu2n0?FK$*2m$j|m zGe@f&p(bVCkQHHvNvK#P{{jHai5jMady%->n;X7;WtSsf>LtLb7{Iyy5o@Hg$c;ej zctwx2h-Nb2QKMi;ioq2m!sb1oTOlmxk>7*|>679Q!m#D`z_Pgu;_T-Eeh2*w(Q-m|xP^ZYN#eP~WMY;&>d zu3&mC>LPx4m%6|PNF}!K%+&C7S>#%PT3a5J@SnJ-K?DOXx+vglYxU(Ca2U5WH!Ur! zlbidy2HKu=F^B19b{>8G87Sk(}OuSA#NA6`t}itv23 zs#OaM`!qFoYrunjF~KT+#df`P!8z?>a;R~Ys;hc3XO;hk{{5QsQUEYZE5EsQO{gQA z_(5}a(|5&JpC(HmVE8DSZCs;x zzOU_?%PycVX+9z$vJ3w}?Z;xIdBq^hfP%*WtG`j$+MeH6J>P-;=-MXcw6ij&ha*mL zBra%xEHXNc^6F)K8YKU*$0hyQ8TfV$XSR4aGB}B@tV`+a$85;ul0@b0ir{W?q*TdI zKD9vU233;-q}o)iYM|}NXU^I-*eG~nernYOb(k1@QMpQ4P|XQ~U4zo6XOam( z?v*ytG{SuFwAXyG^^P79$dfS@)BzE_BE%WODW01a>B}Czb)VAStykfxJE^}6nXoi+ zj+)6ox}L`B-OPW}II1F3EOYEzdE%s}JIAQ8k4!AW3VSrl+L6~7X2s!1v)NH+ux`YS zb^zU4Vy311+vyutdD9ieQorLdZr|CdnM!KR$%Y;wc5c_h*fJpDS~K zhiT~Tt~Xmit^haB6uj!@8O2pLuFT1fT=u$rJgp5Y+QDE&-yOB$0Mp1hp~K20Sj8(g zO`q}df!Z0ajC39MkmivV)scS0@?Z%4jLG@)f`jq4|0`d%{6 zMwdP1j?;c#R1=O_?6YCxB1gL-zF}2@8dF~V=Brz&gDo=x?y$anhU+3%y(sfEnsJ2+ zXNS^5=Q!qUy`sBxKs_P&+w_QG-F}L=q&`LA6{|a$RixJ}B4%{a^Ii zoN8UO#4gBNjEDWhLjugOyc`H(_g>ZA8OL52A}o({$<;fV6l?~JF4Qs;9=`kb#vWKU z1_K`WhB#)F2O6(5l9eF{-Q&gC4D329QUHr3Od-30Vap%RpkX|MT~xe)YSjC#0;7iM zE;k4G8)>&7O&o4;&8yI-FnK4G&@B8YabA~Ah1yy~7qOwmxMlKy7Z6@aAP+Lvy2m2F z)BZ}Tc5mH^Q&absmnEF|_1|i`lo65x-#*w0K6Zs3)2)(_UYob%Tw2A89~MWsP?>YA zoVdUx^HqT*+@@|)%n}f)yJQsKi^3@K71&o#T4KgoFu|IGPOwg#>@eIxMJ*HBD9gv~ zWHX=$*P3%hnxsoWQCQ{C>MGKQo(5|5Y+;fcZmlZQv5&it2W-R`HBAZV9}*OyZ%avs z>9SeDHq2U1wZWTpHk>Ztx_@m`p^q|LwM3}uJZrXrA-D>F;)8wIZn)gmuC*reV~J@3 zMyo!Zb)h!US#e#;A5*r|jPLrsS)61?2addo3W})kgWB9|-($9I!aCMoOM)ChW?;cl zO4cYqcxUzBV-E-!$+?~m7`EuQTk~E&jW}F|bf?9DuQ#xX#n#ZZs#)n1pa`$&0bD_Q zHfs2%91(P>yVO^OT-X3-gp_aBmzrF#AR2b$I4Dq5nCmQWY)|ski=?KuO9_owv`Ngi zzEL`u#pp_V3HsPNnj%b7@KcpM=lM`28=KX1a0?yMV!muGS))|c*EGb{KXyjRwYI{N z^mB~u_P!omVDBo!+H1sT7r(uL|e?}hIpKLlD2;Jxv{ zMLhVtc&z~DnJqpRTmKkI4!Ugl7jl$PisQsf0%REFy3`%jgpVUlvT$p!I_L*z5K9>oSGTb%!C3Z$7)g>iu<*It}P)6 zUX$_BFbunftV9)-$(EoBi9x)XKo5;Xf{EB9a*2ZfwS_1C9Vlh@c1LZ$8g9AyKDxyW zyAO?{J=4%mjy`_Ax!z@Bt+-UKpgod&N+Yl7znYa9I(Z4ER5bqWyBl2(q7Ci#{L;d! zAw5-FMNA1K%_XG3vx8^v4MIG}-+aRhXgFs1~Q@W#j7T;ijk&Zd7DdQr+&!Z<^DJ4xcW>S?>NZt{>9``bGGC6RVaW z$XcZFWkX9 z;cV?-%E~TAp|;hagUi~+>naCnpA&6YX5|Qw^xfRq5)@m_P(c2^$8l8y?v%Lvc*FR{ z{V;#*yK*SC8s4EF=o7nX2(%291(g0ojsa-^MF;^rkuh}tES$HI_3R-3(&}v-yjvZ* zJ8%^tcrF+GMcn(K{HAULK-1UFQ#8Y9khf#g8dXE~>CX(?A&JR!?@b%Pw>Czz!`tS? z_C(u0=2YZ!KUY&$ioxwo1iHyjNoco?i+%yv(9L!34^QIPd zr^{ME30L8F=c*m4CQ=S-uG1LLEWi$=!tKhu@tlv#^~u-nUy^(+p<+q0IqI+MYHp$i?4wU7 zYG!WM5LQ*B6{A?hs~Sm^%=GQp6nj<<({!auv3&ZfC-H-!FDvQ~u~s&hc)Da6?W{eq z#f-vxb|LjYf4nM8tr_>~ghPgE{7@2;ebvm*siz5@uoTC<&lfLNXOcMnnZ~AHWIZmK zgwfyw_0cn&gOpIExPJIIt)bdQYIXXZ-iQE_-Y99k z)py6oBI)>P%2-Hpl>bJ`7r!!P_8nt~L_rIVx zgW3Ng^%wz1keP;Gr*BG>KWKGsJ`E+px0kBlj*2mr`K_^UQ&k%UuU7SLoiO=l9_!BB z8m9*3{4dy8&0umG=9#W7YyxAjs5BfPzYawZ`SBg3eLeP z#KtkFr7FF|ikww#n?mcp7`d(47B=AkVF`!B4#9yGY10>lr07bS#E)!8oUUWr)SMg1 zA$uI3L@BE7p5#ii`Y9RDS}^b~?iU6-gq8LaIoe=?BC+wE^D1;0DvySojHHYG>uSy| zg4Y%3pj>Xr78Bpz!2^UmDx{`!N8LS(uSmufJ$rcK>LF!y_F2c0g-t+{CBWfRhMRMeittR@E8T1|Ji|tucHiH;{`L3BByj zhjvrABT!#IpWPn_G~p*Sy}O_PE^lO(gV?oROe@)106@grTQLdnCNR)l^#9rG7BqCaw!@TU|US)}qJZpSU=Z zkC-fBx&%IaYq#OuS(CaXheIGtnSL~`hY=4NRu~YBGQ0jH{|i3OK`LyBL8zM^V&h6k zVWaZ+XZZUvDbGjSYh4t^MK8p|0W~+KdQDLS@R4mZSpn|7!e#TlS7=a6Ach%0F@2Rq zIsBEx=cqwnByWBMT$Zz0@9ip9K#7Q+K3pZV&bb@c3qhFh=|Pwy!>y{Gg9qL0M{~sf z?Iq2fdj-8=7xy!=wbm+FU1xK9;4E&A1O-u?{DUAccHK#oY=iV#x}rB!+#f6S1~e{4 zq*ZE(o!O3f#z5~A51FVs1gzxX&*vW=i1YX!<9+dvKwi#8Dh0E5bX3R6o8J%AWR6{ z1F<61wl*EpFm3iBXMO8E^hPK_(Q-(4HAaxyn&38t#xm16%!$zrOp_%9Uv8Om)6T}N;fu=cK^JU=1NfIo`g^@b=Y+U z@BSQ97QMPzxrXdMa`)WX?(O6VA;?PtGISaZU|z?D!XGkR{ivrNl9Jg`i#dlk0q&43V+S@dPu>mpT^jdkE@oYn?g2&)Sp#B zR6|nr%IZ9V?I0}>&OFM|y_l6@PeHd&KIx2Kg7Lw4iO>h641u^uFZl2F*^9V^soa;8 zP8_XughO3}dsjzIUbb|Dc?vYIj{S+dlxSwB-6?K9V1bPr95pGfhE8aK#!a!&zi{oi z%@C7?0y6?I5|Xm0t1rU>gR?Jwp+qPWnfccQj16ewvB#Q(wCXB%)sYli0%> z#b@}Pc_QYvSn`rSOvYl4B2Y+b0}N~ww^2YWm>8-se@4{+Y{DV<$gz;{nSdXXTnO$7 z`^YXfbmh=_zQ#4w2mkb$VXut95rvLcq;M)&2M?Eq#<%T#_rx_+41aI=+0nJwf&yaN(+EOL`G4N_fDO@sVYPK&$uE*_322` zDh#|AvSc=Z>96XZu}CYmv7P)VB2xlNl?WL5<}{+$)qQc~l(UE^-vwck@*fb*So#&U zprUdPwG_9*uK8b-*j)pz?S#cw<-p}XelImPYy$YAuvjt82V76%G@G7lC@CZ2)?1GLXnWQs9V2a=_xn-Q9Wu0Fdhge}8=nd5j1 zgk+;JXws`Q>pbPY=rb7wVlY>Na^+~qVJ5ykmPJhIECum+a5$FCoo7D%$|Vt}}- z&d94+se2v+!dhBhB^_@6z}wQ9mCQJ zU?0}6SZvBjdcYcDAI2p0at(a_SJkvXzR!OQw@+=X+D!#+Iim#Juma;>kNX5rD(s#f!>Y0>`v@rZ$jmU3+)=UT$e>Jk2hc!PG}XGx~v2cnw9p&Tipu) z?Qb&*$5^%vH_{2jTrPfNTj8-Gn69anR7o7v)KBDCj;azMM|;_O`$I@nUb%ERj6i@! zLZi5<|KT(tN`B)hlvI1MLHNj6jWkz0egQF{K zjYV|2c5PEbwY}d;n~ktTt&K;NgBnxS6JFI<9Y|krLu{UcC50&ZFzt zN%Q9R({04xyWftkKfYEighg}aN6hRSB2^KrURlh;|4y&Jka@=KS+d6Q2@A5M2FTW9 z--*#jHDYh=0gyouO*LmA%B^IEmcywpcRk3d7ZI*`dW&&*_s3dECg7zFrqvodkyR0J zme6&kqT+0Vanmxx4i2<_qVJ_~l(OESdDX7RC zyD<7;=5C`V$dkExGi4z^a-;C?Cuqzw-0``21{${sKo1JxJbay|P(4s0BLNuM?to>` zOQ%U5T{UeZ$vfH%PcG>`>qBFep$iiq&B6BQFT>rP(r~V<$g+|*c4GL*Gs2xf$O-^ef7!QpCz`k%)huk`_3p;kd z23Nprdqbl5(XLnn)ogVf!3K}Q&x*R5rc{vwhd;M}HOgG56laT|tIHh*_Pl#GjFIrr zgt;rfoK(SL^d|PA7s9LM(yj|}e^Jw_(SqcR+*--Z(`Ho!kv@ZNYakRGGrd(r)|OTkR0FRw@=9` z0Ky@vmbGWOlxU+dF4W4S6!_=k4ivVr;gotSCPu16*f#v}nisb{NHxbh@48Cy%9DkNHM@Wu+L^miH|)MMXea3<0nW6T2*SZsO35q&`iRk5)}7 zCqB*S5LZ%VTe7~JO`-SlIJ$k#Wo{Kx?^LPEHf0LW>8bnMa8qVC&@SZ{-oa!|l=KU{ zN-_S>A6E5yKQK=hqL(|9ItgTab$fj?1*cXU(#2(fQAd%VWf<6(f z{iD1Cj0G35FB9!TZp7#fhmuF=g95xJ+VaIS#Jf2_#3m6m^cG2L&6!svHEDNg890>c zT=N^*(64B{`zn}VO=OHsjtoY#xtSXcFS^+jl=s$CsiKDsy$BC+4f)K{`n&EHdT$@o zw`}DxQOGm(a#92qJm9DE1?)HDPt3;t=Pf%L@%yUAp+P`uaY6oT%MNEV7Z)o#3+KO_ zScBHuk2MK2UjxHGM)^ksl~?j`3E&(fn_O9SvA;h-qRw$p4-v`PZ}-odGtue4mkhz6 z?Ms_}Tl!FQt-qg}GB`UuoOuvk;aJ1)71iLZ9$5p}5H?j#<(=Esw&4u@r1@20yYBX| zAGR=M|L3D7Pa&%}mdpmvPDqT^=G3R1C&r{%t0w$?`_#r`&aE1B*LQ7c1*YbszislB zarlzM{)_={%vURAZ!QxRpkcv$;pTdtHK8?Nt9R(j+wfFqvC{DLWOg|z2rv1^;(jt) zI*aK0QcTj2>lJNnfo+)R3SxA_YmE#z7IJj_FcJ5S=C`q~FBF2So^DU`kAs{)1Wj@+ zj$b+4`$1qUDpxjmzGm4QmwAvJ>y&K?l`Yj@Hex}ZC(9{fjfI~=3MO7~M|Y>Z37j3V zfrE5lxORCyO*i;9)qto`(Q4R(D*HFs`Z}+=R8Nseuc_yM``(Sz)$r8Lj7OjpKB>L{ zd}5t&e)HH8zW#(VwEcEBI=~vEnDUm+KB;hAZ^qt6_$Xj*$lV;`HT1K>&2I~2`swP$ z5qr%`D@dEm7KGGrY+;t%%VWuhu;2v@l~NQM~}Te|!I#&L(5#lzu1r?TAht%(d_tmk{AQ*^tqhfYTPg-QJgBtCvuSxM9KRBv;!M-Ydgc3 z5wl%ZoqeUa&`g*8Fm{_1GI1XJ1pnZkncwQf2P_wEElkgi?^Nq|_@g=R^D!(TzeW_k z{t%*`Gn;+#+g1SPtCaz!UDPc)8vM}JsG#mVdNOdjSOtEN(&?^1&IRl0QP!QdOSok zG36n96oO{ZD(3fME*a^fWKbDtj96uOYOS60dz$a6mX~1iLSuI2?-W~#epN)wMcBpKUN_Sr&`4M%smK2*sS#C0Q>{=>q ztVNcVkto|*wv_x#gp&jserI+PlQ{i0N*2gN_*YDv(3gI8Rj`L-a3n5mcs|lqDK+f@ zgdYTU5mHmbGwc$r2?YaH3dB4ur8HQW;x+N}QuEfRr)v3LAtXi-#%kD@p(Kap5$X}{ za0wz-g^eI(91yXMLWVDTFsHbtjIdPdFOA6+2Rs#>rbfzlADa2qP zDIbL;Lb83QB_?{B#x|eGDE7sT%&1hsLtzmdTNHTy09fpgTX~!+)p#?+RXsJ4hQY!l zDa`H9A~{yIILe^jZ>ugBt<`Aq;sg4HMPRXksmXgQiW(wxRnif|#eM;vG%6LQ#YChOJV0LJ1YszKl;(M4lIphfaI_Rtw zXxXH#62&;e#X_Dj@#W$Vz_<6c!}lN*15<{Dk%P|GBDrvEi$hAG?gA!a5t73Ld;|4{ z#%1xhFuvK%I8psCau-_-KoQE0*jS0R2t{I@Pe1f*< z_W~fmnu+7d(aQ+Qz69?QE5P=9!z4E`Tc(i5NKg*l_aqdP&IgInwzvcMWcpLki!CY1 z&~G9!LH(!{!OE|X$pdKKQ&U4&dIoR){y&@|xg z0Az5Yp4DW7_jnd~c8+byVt4RRszTXPD8*URv`23UjWdd#-D}ORcOp*fnAD<_@HJbw zw;OH2bGNqwIE<$m>mpV|LO`pBD;%oP(hZ(v(d6h)V~G&_&(o*`;o8cNm{qbD-nz!^ zF5i!_?KB?DxBQvS>ZhNOB#th9d4A%Z zt)mg(d(_{4Ma!Kt5AjDd)P-01b!e2a-*WhPH)$UlskCtQE(m|gaK?iT+n4$QzENFq zyThc=KZ3dm_Z)8IwIW!{8v8P{1Y@7e(P`THgy5D3j4FIVb;}`Q`0k^%m&Tr+H}y7P zCeQHw_E$+N`*ZkS19uN!b@<5x7B&weNs;jYL1m8Gg~mlrE_6g}F8tvQc=}aO@jgudK-L66K#>16hjn)Gv@!eJ@z3k%I2X2{ z`3b7O5m9$r7+=#%S6O^@OiWTU`?bQQdW2&_aR?EQygg#-|6#>0@|z7UUUuPJ;Cw*j zW9Ia-W+CG6NziX7;V!sX>vz&i&j$-W7ai;~k6i9y@D*|zv?Msy*oWzbB%je;@7FOX zf1i&FgYNEZ)0gi9EYKi{=j2F-!ScEGib%-yhW*BZZ+*>lfzfe%u^;$;C5qv7c>-5W z(4cY6euUM1)Z+%oojVRZ@k~}&=%3HnQ0Lju?dcTUqeLZiU}CwZSq@n#mW+DS`W|hY zC47cjP|-gM!5AO=#d=#zz|g~mM_E79{R0Q_F>wjts8QU*pTFTFW^-}1`pI~l){ru7 zM)MN-K#o;;I!qFN=p|e1T6m?6=G|g_I*%mrIqNID)4}w=Y?H+FE;pYKwjR|Xw{>PS z363e&Puq92aLfNPuOlu53*Svm$m0V&M;6RP)^xh|ZT0`c-T=20a9#>MHU|nrxC=Mx z-v&=#fDXcpYx<2yzg)kA%L69zqH(>cD==L3=u5xiA%H5*@wLbI@(IJl$UqR$$k z#m=}A^8GOnQMSv%Mlbe{Ya}Q&G+3AH?MqMAR6%czV~WeP+%z=_sU*siaMg>?5UOX-5eLwg_wqKe zf6HbbTT9hT7^bt=@yQW4N6#X$rq0DFW~~`V;RxEOCo1p|@-n~`H{W_G|C9W>CeM!` zX~H~f75(;o*m)i0VSU%AcUtVD%#5(v z1zNlg2M`9J7E$&(VuUjqGl&%{M(K={A@TZ>B63ej8+t6RM*2w!P(j!6p-nvj7B-=% zqwpxxgwPA%Maszj@)IitN>l=J`eGEj_N27p zb%UkG847A}M)1mz1;E-b@75=jd8m9M{Z%&-YUT2d2{`sB8emiY(tlJLdhx#`{C{dm zN{j0MMVTgwUI1gv1)u_>6G01;Q-3?1KF9k-( zDF0uL{d)x&0cbazz?v`bs@BO-(S`!IdJLKds@LX*Mjse{G_#d=g5dg==are?amiR; zr1Es0P&`fF|69w>z3i55F(!2CK4;Qoip$?^eobXVj?8EC7z(W#5Wk1|@cqp531+Ke z&K%6SexN@O{;0w--BMEWNX5lT0f~Uxi#76izUPoUJ-vTzA?lr6ki6lmRDjlGfie&?H8?Iwkuh8y!$5_;g7F*x%M9^ zo@OU`c9qr=Z12+Ve1+eOkcdt*PhJLic|H)9whwQmt6^t^3tZF=&=oJoy?uJ?+%;`{iZ^S^;KfJJBN%bEC_mIo1Bp={WEQQE z6;^vx1t1;nBU*}FF3v~AoU^LKtM-2#l;3_05})BDVs)BqJ}gJ|M&~99SSM-fSjgAl zoAR_tRjm6n3LqCo=8Du@kHCjphM$B zg+jeog7QOtflQStq#0y#Bo6BzfhZ2^Xl4rc#-ElnGzxMfBYnD3G{W~IsxGhXVX1V9>DmsIf(hE4hMD9Y;zqs3*!r_# z4-V}!nKKp42GY%As3e`1*eB9s;&j+@e(2#qQpT*5(W%O@{^)RwE-|ddl;7BKSOiq? z@(I|@EV#yzY!xQSkG2t(t+AM~1$%A&V98Zb(o1+_*4{?;R`1?!XA434_Dn5OdiYsZ zcKr}p7BZw+zNlXuWa99KIdp0EVwlzo)q<{=^guNxk=S^Bl`x7v-H7X^7kY-jbrK=FIo z7dMW^=(FyqO*$>kA#v`{6)%n@QHN*sT;GM0n!IRsn$jP(*MJjL4w?B8N5 zB0I7WI`_-esif_2crJT?`$=jaFt&9HXulI52?&wjO4 zb%Fi1!Rj#&c0>}!z5aM-DGH)aLS_OnieXebyLK4zy$F*ZK+|RTnv(YB1Q3jaS?uVC zvhYK9%-x0-e0V3V!7u=C)DmMPW1FrZW^P|n`pWl=_iU}Mj4U9e4*tw-eFOhcVO@Wl zAw(t43^j8j+^FU2$xZggmZaJRUJ*g2B0?qzCu zBH7J`G}@8i`yu!rhp&q7f(7hfKtSxEK|#>|HGH*mwKX<#0=~O6mNi$T{;F%sEZerQt=Enb zjrxzv4azsC_Dr*GKKt(AndbVBU86eP^X|T7eII;t=Pl34MfY&%*UNjM7kc+WaDq?# ztKZXfT-`eB+?(Th-%h-y{BAg8)2r3Z zEHye8Q^f?n>8E`CShBh6;OC!oo3Uv?J~L1-IpCe!9_dKG=(wGClK{`Jdb^vm$#S#P zxNAo?Sj@6{tPP>^dMM1&CF8y7)gC8wd$Zx#^7ql-Q2Tt^x#`-gtNH03E!pzOqSHrp z(-ZxgWOc;R?fKBL&gds+Fnds1vv&WSQ(A+(vU_v&(rD&a^?d2qs7Kg6b3v7KQp8DJ zO=oZm5C5F>hxFaKTQ4Z#hP39&Gj}Q%)JCsKaPt!DDX<``T7gJ_k1_4!lWn*3erc|F z%m3}_t;%;_)ng{$TX6Rw+hwJKg3Ln_5sE0N}U!jo8c)h zy1j!MEC|@zK@(%v(9mn~P6uP_8bbq%q`y6N#1eo==|qV;S?atp@~NEods?axugF-k z$h=>YUt*SxaIoFpRsIjtn${j^7oTEU48Zr)3`rmBj_{pGUCGRlP zsZ=`vghW|6uHWRoPavK--DR2!mc039=Hzy;6!v&X&^55+q6O`I){Z29wrWi*zT<>; z;Fv`-DlUNh>X4!iyHTY45gPoi-pAx&%}r50Dc zldN@?#D!9cN3$vs`5VtOh;!T=`StK-FZ&wLa~bY5HQai=B(V4;N!MB>(ss zE*V&qI-Br#O)Kz$==H4M4W$hsV{yC_ZC%8YclB174{hp$TDIo(%!EJGTU2wfqf_?7 zs&pU#6(muG%*1c|PxCxXgKFxvaT-Oz`B_fBb!DoUONzEw{xDgVHiA>4=+B1pU$I45 zrI_mZl^|n>8B9kM;W3t|(Q1SJFe%NQXy6bpd;kh4BZX@zvGQ&I5CC5ife=@6H9>^a zrZk-NL< zVZwRGde-J~7hcqarS#QVe(jU(s(eok>=uEEY#X!6TEbnqDTle|R^^G-CL1WVrXhgR zoAY7So_CjpU#GK<{R!uca_(5_a@Z*0=0hujJ()ZSScJJL2X@!}wRnzVsLwf+A2xDu zAE!=I+{aB3?v8>l-8)tV`h544Mk22bN>y-5vXWAg z*M3VSXz@+K$oGk-_Jh4owl?&R(DtHusP5P7bHufc>VL^CAis}0+)m2Wnr7K-AmAgf zq^%_XOK_ec^j5U7KaaI0G2gv}K2y52zIy1^!*qST*JA?F+OMo?K7$;be-wK#zUy*M z5ezGK<3+8Q$-;Rhnp_k4;DRt(_JhVQnzitRN7#J z)JS`m1@Nu5wo2RHNt-QH?$_aj$5__<`G#;WGC{xj-M#E*bf~X(^qb_gKZB2Ej8LKh z;!rB957%XaAwpMMQQWtDWr& zc{qk1gyWQZxuzNqW~GZ!2n1e|%dUCeE}WK1|- z%ux`ZlNW|PLul?m+J$=P)r2TyR+R+nBx>rz{twanw%D&7zA$LJDoC?M4-O~YvWn9d(w@)$J ziRbkwx;7aRLJ4u5m~I-~H&V4g;##p1LQHoSq+QefaQ%Lf0~iiF6giqCM7NSq7Xi== z+$zl|Nn{}*39`I^@i?KI1%_oi8I|wWmx&jXQi+pP#B)W(E*&77cA@w!How`dmD|;e z_d<`|R1;})THz)6ofnA$VQ!%AKEbB4&_SQQP$hHPpW^dD@~SQp2Hn1zW>y44K||oXl2d90HvXo z3jwpff}jM8#NrLq!bIrrKLFAF<~-T3hf+cQYftTGy;8yIrX#lF?6PA-|DyWOM>qUF z0J&Rs6e5=OWQH3)SdAF)j}z8t)yof7$flMw=HyTh{^QX37+?GVEIg!e?VhHTUBx@_ z5jfK~iL!xT;c7-IpUEgHE^Z1}015%D!k5vnMb`EGK(9Z%PLSAc3LC!x$Nig;bm0CG zrt#No`8A@!2?{yT#HZ2WI2F2t$V51>?oq=*?YOL8cc}2e1Q%h&x|CH`h6~<$rzC#s zB$Ga<@_D#+AB4wd@@Ab#y2BJ#rYFB0{p#{?9gZR562YqJIRh|&23ibZ;34Q@(U<_% zVzT`mN6tO!%UFO_^UfJuVlNX^c;Ug$)DlmVvCWx%+l<*|<`?<}Q?Lpquup{=Y`t3z z%PPV_49DdXq{)FLl9E6e1B)CjOPyGzih|zw!T?S8yvE^pGNceF(P|I*`1lk4xgVm$ zF~5J*PtrAX?v))D{ZxgyxZn~4NmzC)>yunU&#@XNfsipBE=R(Fp;x~nfPM$e)H$t_ zb7mq1*K*o<99MA?4)%AIE{It(t45(X z`~4NOxWcGLiFe8rn%c-Cv`DMau&zkmHzXA3)P*bD0?<2JaPn0cPEZYhO_(QQRycH$zyhz<;drPOh$ zeQ?h)cVom5J(R3l%U74_2Cy?g_E^$$>!y@E3~TG^4HAr{*+Z+j@3BU{qE6#cU^rfVs5{>=IBD~tHlIjCB=?T=w1M4tXchmv=cHH>?m+3i`vK1tq?42_|-%A{j4M zVDwOBGOA%#Xj7n29Esb)#I{w2bJW1An+-mziYU!!H6{;^ASc((()JA_FebDqwg1qB ziEmZqxl1A{Bh!fW-u+4-BKw!iVVHk9WptdM4#KZ{@rX6>w}Mtlg)mk@xvM_L>sPb1vMyr>g|TF=7&wY?ZC`);}&XIypZbVM7f z7M~c?g35X_?@16M$j8DChxoPtTNwG+M;GG4pqWyrr0NzR_OqnojzYwU1DUzPfsm|9 zM0D=KugRdJ=!t;kbp(dIPO9pq`0@VBPDk{@DosO@ql~e#wyJ^!DH6(a(RXNUZi>_f zcJ~F~FktzwVSxHDTEglt{fiY)h>r4A(4m3|@p6D>i2kXZ8we!0%LPcG+d!fQv!IAH zXES(79gJUwI3EE*KbPfmTNx8BRiWk22bSnNqeK=)Ii|Er1^}l8qj~{WUXf7xrk0R> zuRg%0nK<{Gku?=xqQ3X(FR)4g(kC2=+L^-1_=+sXDbUq}N?FKQ^y-t|R~5Tp_i!uo zINz#3I=*}jAx`EkW<6q1KiUNLjOAd)td>$5%qi?&UVcD#_sRGkP`gI(fX9+nXvuXu zT!889SpZR-Vcq7F~6Lt5q@jL*!c9IK6Jn}|LDjBPL63)E^!;Ku6SW^;A6gH zNF+WS!(ep##@U6(NPD}4#6ZeeoX(iI=vH%Ldv=k zs;_5CHuLV5ur+-{Mjm>U8Ds3-t5s-SJ9zH`zZiSXJ^Df2=Ji||&UnstlKyeJQ@`T# z=f-mosa>$92NE>-&$Y+Tov)}*L<-P5N#OqQTVl-^X2a`iKN=XXngAjJ-gqV-pg*qd zzN$d3uk|!wG=AxLgZm8hCPG(;zWSP=A(Ahp&Vp!Wr&X zc1d#%((RitCO+fA)hF`Bsvayl-n^u!OlT0~jM=$l@FHYZ7m7`s#DU~v@ zISU%}q7!OyLCX(ii1rF>2|!iQThycP`rGo?3&`dd@k+!?U(hpC`nI}ho6Fd)aj6%y zuNbuf;`h;Lyco4h))98eTC^wl6eVyape0e=C-51km01ETv3_!=wsH>vA1SWFFc54eK%A2K?T zFM+^q154C_6f`ynHHe9;vx~j0rmYR5m5Z6Jl9RoI^WUg5F@PL|I-eML=KudEJHFRC zgb5|=Dzr;9y&Vr@qdM9V)zzp*7QWMbuM9Lv3Qbn_q}{%#^rEhGC(G#YE#vmS-266h zQMwR^yd4|H+Q_S163V{j=onX;v1M3UfwM^_7&=+UvAIW&MkDwgy10|5VyO)=fg{+CLC7za~iQO@EyB|_>~8SF9B zeEgruEO9(q?u}%pu6p(^pSD6zMirv=#6+)9^Dh}Pw|_VggWaFSFEN=$iBMNJG^us_ ze8!Voesjc8LvG-p<XU_9srE}fmXh7AuYrEE}< z>4e`0H80WVENDMd-fziFAFm=Ydjq@-n_ixR1@Z}Rt;_&gSBiP-L;;Mv8oODbm7ry4 z43cgGPuu+UbalLL4ZaBP0Gq00mss7&X(FQA8Wz@07=|HXZHsp~)A$u|rk(6iIwlW3 zn&g*?2;1SMgrWBBP?PC`_DA!lL9|92!uz^&9jjFO3>Yq1EA%7#bV8@hTUa8Q4E#ka zoftSB5wYL>adu5l9bait{c&^?23TQ-qO`@_Nv4jz7ka|M3Uxmis)!&CnfM4l+&H0_ zRpVTvBB2gVqEcWYS15oFo|;qRnP7}MQqPMK{TB|&x|Y<`ZOVc9g(lIP1cP3(ZXde3 zA$uAUH8-dW6rY^OB&1V`j^L)qNuzxBb0q6!=}00k%&Pn>eRI&)INY#?Nh@2c?B<^+ z`o^{~u72N#tm^2iao{3bEwibhYWD&Ho!xAqpEUPY-tkQD(rv~H3Rh=5pH;}WFr$m0 z%GMz#9;cVDn=w^4u&dqZ-hCKTF_96U??91MpD%Oe3adQ@|=FNob*Mehs)jWW_Dj7WJqFehX zJ7Vkw3#0f2HP!l0;_u+cPKjqiHN0O?RuQg0=p1nxNZ%rx1RLFaek^YSp_|Vau*-wo z9mHA|IXuY{4J85zXB?QN_f~HSEx(Pi>+e?&p#ANhK3mI-|IsFw|JJRD0hX)+zhC~F zNBbW+vtQD`;3gUAWfbT&U=${#qVHwKDr5n|w91$GmqhTJERY2gMDGtFP1C3?i zFj~1i2Q{RdWN;`bAT?fJ2S;$qTO$X(ULeaog<2cl0qX;3#Ld)tf`at)tlSF}f~{@{Lrzk7P*1Zh}ls8|I%sevpJnl-k1%&+>HaPOfXX+Vqf9~{q$zF^ZF(Bymqi~sMt zF>_#MY&9ce8?(QR&V1^O-6|7$*cH_U0qIt=qX-;&ibgV`71r9|Xw!ES{94Sl1Y6)l zg!ji6%V<z8lM|*-t&xe=N=rd$J!P^UZ+5Z5CtaJ`>oN3vAZfAVj&yCu#+@xFu zk>HoVpIW{4Og6KAF^r7U6%-h&W%%5C80td~D%cTI1num_NY=23BcKC`I=w>E|7dcR z4UXc>kRI7pOZ%#QwJz@=PYwy^yAFk!qXjue(*EqxAh5$5FXVs0s$=mnl|K!PHQoKeDt<|H4v+@ zm*D)4TWj}`-3#loTg%|Igw)w>^6tbXq0|F$;#1~|sp$(;G0A;=Y?gcB>hrc)+-8ke z=S%n-X4_Hg*9g?}FM^Bp*6%w+H^@T;#}Hro^~A3&^Jicq<;it|K<8y_$PmoZ3rB}Y z&bn!F!ykro;sR&MMXbGmF1i<9z@mPae7H>XTHERRPD^?GIVjI=IGy*!+KG;fQIpw( ziu*GSzrQdg48}!S$G5b)w}$Ut_=Sz$f%sCeoruVORt6jVBTezjF>t!@rt=-~KUqA> z%`kR6ptTzp1_1$vvizgF{{gdpvvRQ%H#avkaru9^)+0eBzo~(3o!!-Oja_oy^3n0h zp$wiW77b$s5J!;Vq>qO+G!YnF*x$w0lmE`OHgEY$6x_+p(HY*X{Qh7gh|NMtAD2`$^Zv(Y~myTT(L|$8o7Pux^rhE zT`8r$R!yx7e@~hMm^@yiJl{X}%wE)Sx()Bvkedliu(Yg24NQ^aNw>fh)`bi?)Ig_7 zkWUHJ-QRGLp3DhwgWZSh_foMLH|Tg z&Y001XPFHoSxea<2sE=Vf{L_6Yw+RF{FE z>P9m+g({(Ax@~_}b*zfH3^xr*#W}zfixOd+KGCs9N}+sBU&@f+WVqg;zxj|HeT-D0 zg53;r`yxm~r!WSZ8G97u`8xYc7$lfM-$7MH|$51SFB-(hN}$ocSQo{xvy{D4#~Y*T>TY~jNT3`E@E-&8HRwcg;*pk&*r8B9^4OFDN>F~80k!>;?Z0=?5`{y_4|AOM2}7lb_Xbf4qse29hG zhD<9UuR!lOK_S-hka`_SJj?stk7%D{e=gTX_lom{IFOMMR=z*ria5NuYhE?WanWtAf-nUx*vkRI+ML93luoB4 zWgn$PmcnJrN~a$Vku=V9Hsk6j#)?cL%a&`I-PeT7bpA#_($wLz@0Q~$8%vx{8+hR| z!iDm||T?;ObicyF_NgIYOr-s0sUWRS_FZse^;=47Zv=aZMYZ3lI*J6kQ>DrXC-J*9y zu|XjJ3#F-~{I#1wtY87<*+C82qvoQmmDdLSSZqfQtFj}w4n(9$<>?9bNq8O`l7-n=TZoNWbESd54;xkY93|7gNZr3 zx!s~rOwcu4;>)HN_=T?s_@=)T_y_5)vtal;4wQI|SSBaUpHjF=?}+q4Yfh6Md|DoN z{kkrL(yWmfk0SiIxg5)Yf;)nkt%aoL^yFL89Sem}oD>YJMLE z4B~gmS^0cx_y=Dbf`kVKsQd;%hzGZz!?;=4*G`1e7yi zdFDrnB@c-uDT2)f_PenRcc)L(YSgST=vGP;B)FnN+ENcEi|QNkn9doRFNLc6mk-qn z8T2JsN4M(_zAvOA$HCcvR^e1DcFu@8JI-fbsYQY5+#@N`>Qwo#Q!`q{#^1tDiG}9e z#+eb>*3NWaBI!aa7&46Ys}zyKg#1O@COivE#gDxGHrhtGjAewye;Td;=88$qJ+o?m z*If_GkGq!#pZn3d^Z&h#|-5Msgf(yaN0HM6d5XT{u4EmJjq zOOav;2CLXdq>ZzKkp=6^Y6keyG5XmfTjp)Xy7su8?z8VWo(IH#5I{-WV7y4o^c|{t zRNxOKG|ol!QKm2!4w_QV2C>W`8suO|oHus7_U^vCbWq({62tWNzt}WfzosC6MT5#> zn~vt-CcUgV;8&+mV-NgRvi>b2{t0w(6-4m^j3d(c3A!ygWLcIs)DyRTW1!a zA0OSmP-yGMtTnNJSJl8nE;)0G^e5F}9daE28;pBCpTZkv738h5s8fQoX-v!MKNmCN z!$3B~X2Qdp&|YmdWaG73aN=`1O3h?MM`@N}oawkL&&viG>}q-XV>lW~edFfDD!jVd z8}3D)Yo+MtSzsH1DkXf`(FOI~i(J{5b9V&hbHDyV&9VSuem{ml)O5KP(E&!6fHHLf zvqpt_r5Z3IvMac*k?!h>bs1#o-Rjqi>59xR1wvj7Ebl954r}+B)p4U%lWpkLrHnrx z2=E5G#V^VVgfp8edZ?e5wn9m$LKdPcOPnxLxuw14d2?i+)3NAwx}OgXVLV3ms_iYG zmB;dXw%>J=_lT-2y!j!*4GvbmC4IiOYGyf|E=%aL<@7YnOF0o;`dKr5Q{D^ZZdIIs#OiD-;-t+>eT4u$&#m+7d0o!tc4E* zY{g35*gL}F2!!gR&%lHQt!^v4 zE`yx>f_>;vc{Gy>QXwKS1e5%AU7@&n0(MD?-bYN;r}zFV<5TT?o+Q`zO~YV#w6K`L z-QYZPzl5czn!q+XKpHBprFwk&#`iLe%Mqk5ZT0zJWTl0GXZ*%ziu->DY!w={oc{uB z8~*{={$XXUIP!XctFoeZEuilk2d0s$8piXk|I+&KdwMp`&k;Y_lkDyNujsG;zIsi= zLXvI*oIw=;7ZOnaecjT;-s$f(g#TT*1nLd=iAhwJ?Po#{-Gclh`rA9JDGN|MA!J6( z6<@`2Lr?CmS1T`zwsf(T<)XJS4Nc@cHIn9>_q} z-kv0PCox4QxGA)^Nk;y_1IfMnPF2kQFnuiYYg;b?K=wAG@w7&slR zv*I1w?fjFwP2Vi3_qIBD;^(Eg*RI>kaWd_6db#nRXQaQ}{4UF7o+AFu7MzoobiBW; z; zZW{)Mm?S3f(F+K}dO4vcA)A5j5cDI65ZcS#pxV(7Btka={qP5b5e~kvE;Q0`3A%Rl z4WtO|)}b)%@a?4Nn$ef^}p+h!hZV9p} z*yiid%|e~$K{jh~83P0QtPi?w)NyHK-G)`jE`iS(q8ord=8G^wtQs1iC?mk=`q2l> z5c(zSp!(5<&(KXkAA>=dz|nw`9?(W)(9J=uco3#AFhqAF1thE*3h-tHmI9!;H30@0 LV6Oby3*rF)&`LZT diff --git a/README.md b/README.md index 12ad813..8659cd3 100644 --- a/README.md +++ b/README.md @@ -1,350 +1,218 @@ -Aleph API Adapter User’s Guide -========================================================================================================================================================================================================================================================================= - -Version 1.6, December 2014 - -Overview -====================================================================================================== - -The Aleph API Adapter is a middleware script positioned between the -Aleph REST API and any external system that calls the REST API for -services. It has a small footprint, is easy to install, and is -unobtrusive in operation. Use of the adapter confers these benefits on -Aleph customers. - -1. Insulates an external system from a competing vendor’s product and - provides an avenue for problem circumvention that does not depend on - either Aleph or the vendor. - -2. Can minimize the latency of web traffic.  Some queries require - several separate operations against the Aleph API. If the external - system’s code runs directly against the Aleph API, every one of - those queries must make a round trip between the external server and - the Aleph server.  With the intermediate layer, one call from the - external server to the adapter can result in several calls to the - Aleph API, all of which are done on the Aleph server using - ‘localhost’.  None of the ‘localhost’ API calls goes out on the wire - and consequently are very fast. - -3. APIs change over time and across releases, contain bugs, and - sometimes are incomplete.  An intermediate layer can mask changes - unless or until they prove useful, and can compensate for missing - functionality. - -4. Simplifies access control.  Aleph controls access to APIs via IP - addresses.  Since the X-server is required in order to get Aleph ids - for use with the REST API, IP addresses for external servers must be - maintained in two places within Aleph: in - \$alephe\_tab/server\_ip\_allowed and in the Tomcat/JBoss - configuration. It is easier to maintain whitelists of approved IP - addresses in one place, the adapter, than it is in Aleph.  It also - gives the adapter more flexibility in serving other processes. - -5. Provides more flexibility for the external system.  If there is a - need for Aleph data to be filtered, or augmented from other local - systems, the adapter is a perfect place to do this without requiring - special programming on the external server. - -Design -==================================================================================================== - -The adapter was built to achieve these design goals. - -1. Easy to integrate into the Aleph server environment. - -2. Have a small footprint and be unobtrusive in its operation. - -3. Return results identical to those generated by the Aleph REST API - unless intentionally altered by the customer. - -4. Does not interfere in any way with the standard use of the Aleph - REST API through the Tomcat/JBOSS server. - -5. Consumes Aleph REST API URL syntax without modification. - -6. Function in a transparent pass-through mode for all REST API - services except for those intentionally modified by the host site. - -7. Can run on ports 80, 443, 8991, or any other port that may be in use - for the Aleph OPAC. - -Environment -=========== - -The adapter is intended to run from the cgi-bin subdirectory on Aleph’s -Apache server. During installation, it is activated by the completion of -two steps. - -1. Some Apache configuration directives must be activated in Apache to - trap REST URLs and route them to the adapter (see the Installation - section). If the .htaccess file is in use in the document root of - Aleph’s Apache server they can be included there. Otherwise they - will need to be included in the main Apache configuration file under - a stanza for the document root directory. - -2. The adapter will run on SSL (recommended), port 80, or other ports. - For example, some Aleph sites may use port 8991 for their OPAC - instead of port 80. The adapter should run on all such - configurations without modification.\ - \ - *Examples:\ - \ - *This URL calls the REST API directly using port 1891:\ - **http://\*:1891*/rest-dlf/patron/\/patronInformation\ - **\ - This URL calls the adapter using SSL:\ - **http***s***://\/rest-dlf/patron/\/patronInformation\ - \ - **This URL calls the adapter without SSL using port 80:\ - **http://\/rest-dlf/patron/\/patronInformation\ - \ - **This URL calls the adapter without SSL using port 8991:\ - **http://\*:8991*/rest-dlf/patron/\/patronInformation** - -When the adapter is implemented it does not foreclose the direct use of -the REST API on its specified port. The adapter does not interfere with -Tomcat/JBOSS operation in any way. - -Requirements -============ - -1. The adapter is written in Perl. Most Aleph servers have two - instances of Perl installed: one that is installed with the OS and - one that is installed as part of Aleph. The adapter defaults to - running under the standard Perl instance installed with the OS. It - is not dependent on the Aleph Perl instance. If the adapter is - intended to run under the Aleph-specific Perl installation, the - first lines of **api\_adapter.template, install\_adapter.pl**, and - **validate.pl** must be altered before the installation scripts are - run.\ - \ - The adapter requires that these four packages be present in the Perl - instance where it will run.\ - **HTTP::Request\ - LWP::UserAgent\ - Switch\ - POSIX\ - \ - **If the X-server is not licensed on the Aleph server, an SQL lookup - will be implemented. In this case, these two additional Perl - packages will be required:\ - **DBI\ - DBD::Oracle\ - ** - -2. JBOSS configuration\ - The adapter uses the server name ‘localhost’ when addressing Aleph’s - REST API. Consequently, IP address **127.0.0.1** must be included in - Aleph’s JBOSS configuration in - /exlibris/aleph/a\\_\/ng/aleph/home/system/thirdparty/openserver/server/default/deploy/jbossweb.sar/server.xml. - See Ex Libris manual *How to Configure the JBoss Server in Aleph*. - -3. Access to the X-server bor-by-key function is required for - conversion of aliases into aleph\_ids. Column 5 of - tab\_bor\_id.\ in the ADM library will need to be set to ‘Y’ - for each type of identifier that will be submitted from the remote - server.\ - \ - If the adapter is going to be run with id\_translation turned *off,* - the X-server whitelist in \$alephe\_tab/server\_ip\_allowed must - contain IP addresses for all remote servers that will need to look - up aleph\_ids. - -*\ -* -== - -Installation -============ - -1. Acquire the files from GitHub. - -2. Log into the Aleph system as user ‘aleph’. - -3. Change directory to the cgi-bin subdirectory of Aleph’s Apache - server, and copy the files to that directory. - -4. Add these directives to the .htaccess file in Apache’s DocumentRoot - directory e.g. - **/exlibris/aleph/u22\_1/alephe/apache/htdocs/.htaccess.** - - **\# Rewrite rules for Aleph API Adapter\ - RewriteEngine On** - - **RewriteBase /\ - RewriteCond %{REQUEST\_URI} \^/rest-dlf/\ - RewriteRule (rest-dlf/.\*) /cgi-bin/api\_adapter.cgi?parm1=\$1 - [E,L]\ - \ - **If the ‘**RewriteEngine’ and ‘RewriteBase’** directives already - exist in .htaccess, they do not need to be repeated.\ - \ - If the use of htdocs/.htaccess is not enabled on the Aleph system in - question, the rewrite rules can either be included at the - appropriate place in the Apache configuration, or the use of - .htaccess enabled by making a change in Apache’s httpd.conf file. - The default version of httpd.conf contains this stanza: - - \ - - Options FollowSymLinks MultiViews - - **AllowOverride None** - - Order Deny,Allow - - Allow from all - - \ - - Change ‘AllowOverride None’ to ‘AllowOverride All’. Apache should be - restarted after making this change. - -5. Run the installation script with this command:\ - **./install\_adapter.pl\ - **\ - The installation script will perform the following operations: - - a. Get the PWD environment variable and extract the version token - from the path. - - b. Prompt the installer for the institution’s name. This will be - used in the adapter’s response to an ‘ilsinstance’ call, the one - non-Aleph call that it accepts. Here is an example of the - structure returned by the ‘ilsinstance’ call, reporting the - institution’s name, the version of Aleph, and a few - site-specific Aleph variables:\ - \\ - \MIT\\ - \22.0.3\\ - \en\_US\\ - \Eastern Standard Time\\ - \EST\\ - \-5\\ - \USD\\ - \ - - c. Prompt the installer for the Tomcat/JBOSS port number. - - d. Prompt the installer for the status of the X-server. If the - X-server is not licensed for use on the Aleph server, an - alternative method of converting patron identifiers into Aleph - ids via SQL must be configured. - - e. If the X-server is not available, three other pieces of - information must be acquired. - - i. Prompt the installer for the Oracle user id and password of - the ADM library, which is usually \50. - - ii. Prompt for the z308 prefix of the identifiers that the - external system will submit for translation. This refers to - the first two characters in each patron record in the z308 - Oracle table. For example, ‘01’, or ’02’, or ‘03’ etc. - - f. Prompt the installer for an Aleph patron id to be used in - verifying the installation. - - g. Prompt the installer for the IP address of each remote server - that will call the adapter. - - h. Generate the ‘**get\_aleph\_info.csh**’ shell script from a - template, interpolating the version token as needed. - - i. Generate the ‘**api\_adapter.cgi**’ script from a template, - interpolating the server IP addresses into the whitelist and the - JBOSS port number into the appropriate variable. - - j. If the X-server is not available, generate the SQL lookup - scripts. - - i. Generate the ‘**sql\_lookup.csh**’ script from a template, - interpolating the Aleph version into file paths. - - ii. Generate the ‘**sql\_lookup.cgi**’ script from a template, - interpolating the Oracle user id, the Oracle password, and - the z308 prefix into the appropriate variables. - - k. Run **validate.pl** to test the installation. Validate.pl will - perform the following operations: - - i. Verify the presence of the required Perl packages and report - success or failure. - - ii. Using the Aleph id supplied by the installer, construct and - submit a URL to the X-server and report success or failure. - - iii. Using the Aleph id supplied by the installer, construct and - submit a URL to the REST API and report success or failure. - - iv. Verify the operation of the get\_aleph\_info.csh shell - script. - -6. If no errors are reported by the installation and validation - scripts, the installation is complete. The validation script - generates and displays a test URL. Test this URL from any server - whose IP address was specified during installation. It can be tested - from a browser if the browser’s IP address is part of the server - whitelist. - - - -Options -======= - -Id\_translation - -> This option is useful when the external system submits REST URLs that -> contain an alternate identifier instead of an Aleph id. With -> *id\_translation* set on (the default), the adapter extracts the -> incoming alternate identifier from the URL, calls the X-server to get -> the corresponding Aleph id, and substitutes the Aleph id for the -> alternate identifier in the call to the REST API. If this conversion -> of identifiers is not needed, *id\_translation* can be turned off. -> -> If a remote server needs to call the REST API with an alternate -> identifier, one of four scenarios must be followed. - -1. *The adapter is not installed.\ - *In this case, the remote server must first call the X-server’s - ‘bor-by-key’ function, passing the alternate identifier and - receiving the corresponding Aleph id. The Aleph id can then be - interpolated into subsequent calls to the REST API. - -2. *The adapter is installed, but *id\_translation* is turned off.\ - *This case requires the same process as case \#1. - -3. *The adapter is installed and *id\_translation* is turned on - (default)\ - *In this case the remote server calls the adapter with a REST URL - containing an alternate identifier. The adapter extracts the - alternate identifier, calls the X-server to retrieve the - corresponding Aleph id, replaces the alternate identifier in the URL - with the Aleph id, and sends the enhanced URL on to the REST API. - Since the X-server call in this case is done on the local server - without going out on the network it is very fast. - -4. *The adapter is installed and *id\_translation* is turned on - (default), but the X-server is not licensed for use on the Aleph - server.\ - *This case is the same as as case \#3 except that the adapter calls - SQL lookup scripts instead of the X-server for id translation. - -Under what circumstances should *id\_translation* be turned off? If the -remote server is populating all its REST URLs with Aleph ids, then the -translation step is wasted processing time. In that case, -id\_translation can be turned off with these actions: - -1. Make a backup copy of api\_adapter.cgi. - -2. Edit api\_adapter.cgi - -3. Find this text ‘my \$id\_translation = 1;’ and change the 1 to a - zero. - -4. Save the file. +Aleph API Adapter User’s Guide +============================== + +*Version 1.6, December 2014* + + +## Overview ## + +The Aleph API Adapter is a middleware script positioned between the Aleph REST API and any external system that calls the REST API for services. It has a small footprint, is easy to install, and is unobtrusive in operation. Use of the adapter confers these benefits on Aleph customers. + +1. Insulates an external system from a competing vendor’s product and provides an avenue for problem circumvention that does not depend on either Aleph or the vendor. + +2. Can minimize the latency of web traffic.  Some queries require several separate operations against the Aleph API. If the external system’s code runs directly against the Aleph API, every one of those queries must make a round trip between the external server and the Aleph server.  With the intermediate layer, one call from the external server to the adapter can result in several calls to the Aleph API, all of which are done on the Aleph server using ‘localhost’.  None of the ‘localhost’ API calls goes out on the wire and consequently are very fast. + +3. APIs change over time and across releases, contain bugs, and sometimes are incomplete.  An intermediate layer can mask changes unless or until they prove useful, and can compensate for missing functionality. + +4. Simplifies access control.  Aleph controls access to APIs via IP addresses.  Since the X-server is required in order to get Aleph ids for use with the REST API, IP addresses for external servers must be maintained in two places within Aleph: in `$alephe_tab/server_ip_allowed` and in the Tomcat/JBoss configuration. It is easier to maintain whitelists of approved IP addresses in one place, the adapter, than it is in Aleph.  It also gives the adapter more flexibility in serving other processes. + +5. Provides more flexibility for the external system.  If there is a need for Aleph data to be filtered, or augmented from other local systems, the adapter is a perfect place to do this without requiring special programming on the external server. + + +## Design ## + +The adapter was built to achieve these design goals. + +1. Easy to integrate into the Aleph server environment. + +2. Have a small footprint and be unobtrusive in its operation. + +3. Return results identical to those generated by the Aleph REST API unless intentionally altered by the customer. + +4. Does not interfere in any way with the standard use of the Aleph REST API through the Tomcat/JBoss server. + +5. Consumes Aleph REST API URL syntax without modification. + +6. Function in a transparent pass-through mode for all REST API services except for those intentionally modified by the host site. + +7. Can run on ports 80, 443, 8991, or any other port that may be in use for the Aleph OPAC. + + +## Environment ## + +The adapter is intended to run from the `cgi-bin` subdirectory on Aleph’s Apache server. During installation, it is activated by the completion of two steps. + +1. Some Apache configuration directives must be activated in Apache to trap REST URLs and route them to the adapter (see the *Installation section*). If the `.htaccess` file is in use in the document root of Aleph’s Apache server they can be included there. Otherwise they will need to be included in the main Apache configuration file under a stanza for the document root directory. + +2. The adapter will run on SSL (recommended), port 80, or other ports. For example, some Aleph sites may use port 8991 for their OPAC instead of port 80. The adapter should run on all such configurations without modification. + + ### Examples: ## + + - This URL calls the REST API directly using port 1891: + `http://`**`:1891`**`/rest-dlf/patron//patronInformation` + + - This URL calls the adapter using SSL: + `http`**`s`**`:///rest-dlf/patron//patronInformation` + + - This URL calls the adapter without SSL using port 80: + `http:///rest-dlf/patron//patronInformation` + + - This URL calls the adapter without SSL using port 8991: + `http://`**`:8991`**`/rest-dlf/patron//patronInformation` + +When the adapter is implemented it does not foreclose the direct use of the REST API on its specified port. The adapter does not interfere with Tomcat/JBoss operation in any way. + + +## Requirements ## + +1. The adapter is written in Perl. Most Aleph servers have two instances of Perl installed: one that is installed with the OS and one that is installed as part of Aleph. The adapter defaults to running under the standard Perl instance installed with the OS. It is not dependent on the Aleph Perl instance. If the adapter is intended to run under the Aleph-specific Perl installation, the first lines of `api_adapter.template`, `install_adapter.pl`, and `validate.pl` must be altered before the installation scripts are run. + + The adapter requires that these four packages be present in the Perl instance where it will run:
+ **HTTP::Request**
+ **LWP::UserAgent**
+ **POSIX**
+ **Switch**
+ **Time::Local** + + If the X-server is not licensed on the Aleph server, an SQL lookup will be implemented. In this case, these two additional Perl packages will be required:
+ **DBI**
+ **DBD::Oracle** + +2. JBoss configuration + + The adapter uses the server name ‘localhost’ when addressing Aleph’s REST API. Consequently, IP address **127.0.0.1** must be included in Aleph’s JBoss configuration in `/exlibris/aleph/a_/ng/aleph/home/system/thirdparty/openserver/server/default/deploy/jbossweb.sar/server.xml`. + + See Ex Libris manual *How to Configure the JBoss Server in Aleph*. + +3. Access to the X-server `bor-by-key` function is required for conversion of aliases into aleph\_ids. Column 5 of `tab_bor_id.` in the ADM library will need to be set to `Y` for each type of identifier that will be submitted from the remote server. + + If the adapter is going to be run with id\_translation turned *off*, the X-server whitelist in `$alephe_tab/server_ip_allowed` must contain IP addresses for all remote servers that will need to look up aleph\_ids. + + +## Installation ## + +1. Acquire the files from GitHub. + +2. Log into the Aleph system as user ‘aleph’. + +3. Change directory to the `cgi-bin` subdirectory of Aleph’s Apache server, and copy the files to that directory. + +4. Add these directives to the `.htaccess` file in Apache’s DocumentRoot directory e.g. **`/exlibris/aleph/u22_1/alephe/apache/htdocs/.htaccess`**. + + ``` + # Rewrite rules for Aleph API Adapter + RewriteEngine On + + RewriteBase / + RewriteCond %{REQUEST_URI} ^/rest-dlf/ + RewriteRule (rest-dlf/.*) /cgi-bin/api_adapter.cgi?parm1=$1 [E,L] + ``` + + **If the ‘RewriteEngine’ and ‘RewriteBase’ directives already exist in .htaccess, they do not need to be repeated.** + + If the use of `htdocs/.htaccess` is not enabled on the Aleph system in question, the rewrite rules can either be included at the appropriate place in the Apache configuration, or the use of .htaccess enabled by making a change in Apache’s `httpd.conf` file. The default version of `httpd.conf` contains this stanza: + ``` + + Options FollowSymLinks MultiViews + AllowOverride None + Order Deny,Allow + Allow from all + + ``` + Change `AllowOverride None` to `AllowOverride All`. Apache should be restarted after making this change. + +5. Run the installation script with this command: **`./install_adapter.pl`** + + The installation script will perform the following operations: + + a. Get the ‘PWD’ environment variable and extract the version token from the path. + + b. Prompt the installer for the institution’s name. This will be used in the adapter’s response to an ‘ilsinstance’ call, the one non-Aleph call that it accepts. Here is an example of the structure returned by the ‘ilsinstance’ call, reporting the institution’s name, the version of Aleph, and a few site-specific Aleph variables: + ``` + + MIT + 22.0.3 + en_US + Eastern Standard Time + EST + -5 + USD + + ``` + + c. Prompt the installer for the Tomcat/JBoss port number. + + d. Prompt the installer for the status of the X-server. If the X-server is not licensed for use on the Aleph server, an alternative method of converting patron identifiers into Aleph ids via SQL must be configured. + + e. If the X-server is not available, three other pieces of information must be acquired. + + 1. Prompt the installer for the Oracle user id and password of the ADM library, which is usually ‘<xxx>50’. + + 2. Prompt for the ‘z308’ prefix of the identifiers that the external system will submit for translation. This refers to the first two characters in each patron record in the z308 Oracle table. For example, ‘01’, or ’02’, or ‘03’ etc. + + f. Prompt the installer for an Aleph patron id to be used in verifying the installation. + + g. Prompt the installer for the IP address of each remote server that will call the adapter. + + h. Generate the **`get_aleph_info.csh`** shell script from a template, interpolating the version token as needed. + + i. Generate the **`api_adapter.cgi`** script from a template, interpolating the server IP addresses into the whitelist and the JBoss port number into the appropriate variable. + + j. If the X-server is not available, generate the SQL lookup scripts. + + 1. Generate the **`sql_lookup.csh`** script from a template, interpolating the Aleph version into file paths. + + 2. Generate the **`sql_lookup.cgi`** script from a template, interpolating the Oracle user id, the Oracle password, and the ‘z308’ prefix into the appropriate variables. + + k. Run **`validate.pl`** to test the installation. Validate.pl will perform the following operations: + + 1. Verify the presence of the required Perl packages and report success or failure. + + 2. Using the Aleph id supplied by the installer, construct and submit a URL to the X-server and report success or failure. + + 3. Using the Aleph id supplied by the installer, construct and submit a URL to the REST API and report success or failure. + + 4. Verify the operation of the `get_aleph_info.csh` shell script. + +6. If no errors are reported by the installation and validation scripts, the installation is complete. The validation script generates and displays a test URL. Test this URL from any server whose IP address was specified during installation. It can be tested from a browser if the browser’s IP address is part of the server whitelist. + + +## Options ## + +### Id\_translation ### + +This option is useful when the external system submits REST URLs that contain an alternate identifier instead of an Aleph id. With *id\_translation* set on (the default), the adapter extracts the incoming alternate identifier from the URL, calls the X-server to get the corresponding Aleph id, and substitutes the Aleph id for the alternate identifier in the call to the REST API. If this conversion of identifiers is not needed, *id\_translation* can be turned off. + +If a remote server needs to call the REST API with an alternate identifier, one of four scenarios must be followed. + +1. *The adapter is not installed.* + + In this case, the remote server must first call the X-server’s `bor-by-key` function, passing the alternate identifier and receiving the corresponding Aleph id. The Aleph id can then be interpolated into subsequent calls to the REST API. + +2. *The adapter is installed, but id\_translation is turned off.* + + This case requires the same process as case \#1. + +3. *The adapter is installed and id\_translation is turned on (default)* + + In this case the remote server calls the adapter with a REST URL containing an alternate identifier. The adapter extracts the alternate identifier, calls the X-server to retrieve the corresponding Aleph id, replaces the alternate identifier in the URL with the Aleph id, and sends the enhanced URL on to the REST API. Since the X-server call in this case is done on the local server without going out on the network it is very fast. + +4. *The adapter is installed and id\_translation is turned on (default), but the X-server is not licensed for use on the Aleph server.* + + This case is the same as as case \#3 except that the adapter calls SQL lookup scripts instead of the X-server for id translation. + +Under what circumstances should *id\_translation* be turned off? If the remote server is populating all its REST URLs with Aleph ids, then the translation step is wasted processing time. In that case, id\_translation can be turned off with these actions: + +1. Make a backup copy of `api_adapter.cgi`. + +2. Edit `api_adapter.cgi`. + +3. Find this text `my $id_translation = 1;` and change the 1 to a zero. + +4. Save the file. + + + + From 7653e7c8633a2f5d38a960197ed553c076364f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 16:15:26 +0100 Subject: [PATCH 10/31] Remove execution flag from template files. --- README.md | 1 + api_adapter.template | 0 install_adapter.pl | 8 ++++++-- sql_lookup.cgi.template | 0 sql_lookup.csh.template | 0 5 files changed, 7 insertions(+), 2 deletions(-) mode change 100755 => 100644 api_adapter.template mode change 100755 => 100644 sql_lookup.cgi.template mode change 100755 => 100644 sql_lookup.csh.template diff --git a/README.md b/README.md index 8659cd3..8483f0d 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ When the adapter is implemented it does not foreclose the direct use of the REST 1. The adapter is written in Perl. Most Aleph servers have two instances of Perl installed: one that is installed with the OS and one that is installed as part of Aleph. The adapter defaults to running under the standard Perl instance installed with the OS. It is not dependent on the Aleph Perl instance. If the adapter is intended to run under the Aleph-specific Perl installation, the first lines of `api_adapter.template`, `install_adapter.pl`, and `validate.pl` must be altered before the installation scripts are run. The adapter requires that these four packages be present in the Perl instance where it will run:
+ **Fcntl**
**HTTP::Request**
**LWP::UserAgent**
**POSIX**
diff --git a/api_adapter.template b/api_adapter.template old mode 100755 new mode 100644 diff --git a/install_adapter.pl b/install_adapter.pl index c816695..3f07757 100755 --- a/install_adapter.pl +++ b/install_adapter.pl @@ -3,6 +3,8 @@ use strict; use warnings; +use Fcntl qw( :mode ); + my $input_file1 = 'get_aleph_info.template'; my $output_file1 = 'get_aleph_info.csh'; @@ -95,9 +97,9 @@ } print FH2; } -`chmod +x $output_file1`; close(FH1); close(FH2); +chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file1}"); #---------------------------------------------------------- # Generate the api_adapter.cgi script from a template. @@ -111,9 +113,9 @@ if (grep /INSTNAME/, $_) { $_ =~ s/INSTNAME/$instname/og } print FH2; } -`chmod +x $output_file2`; close(FH1); close(FH2); +chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file2}"); #--------------------------------------------------------------- # Optionally, generate the SQL lookup scripts from templates. @@ -130,6 +132,7 @@ } close(FH1); close(FH2); + chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file3}"); #--------------------------------------------------------- # Generate the sql_lookup.cgi script from a template. @@ -144,6 +147,7 @@ } close(FH1); close(FH2); + chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file4}"); } #---------------------------- diff --git a/sql_lookup.cgi.template b/sql_lookup.cgi.template old mode 100755 new mode 100644 diff --git a/sql_lookup.csh.template b/sql_lookup.csh.template old mode 100755 new mode 100644 From fdfc67dadd85919cf8548683c46241a7f32deaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Fri, 6 Mar 2015 16:21:53 +0100 Subject: [PATCH 11/31] Use sane varibale names. --- install_adapter.pl | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/install_adapter.pl b/install_adapter.pl index 3f07757..351a52e 100755 --- a/install_adapter.pl +++ b/install_adapter.pl @@ -5,17 +5,17 @@ use Fcntl qw( :mode ); -my $input_file1 = 'get_aleph_info.template'; -my $output_file1 = 'get_aleph_info.csh'; +my $get_aleph_info_template_file = 'get_aleph_info.template'; +my $get_aleph_info_file = 'get_aleph_info.csh'; -my $input_file2 = 'api_adapter.template'; -my $output_file2 = 'api_adapter.cgi'; +my $api_adapter_template_file = 'api_adapter.template'; +my $api_adapter_file = 'api_adapter.cgi'; -my $input_file3 = 'sql_lookup.csh.template'; -my $output_file3 = 'sql_lookup.csh'; +my $sql_lookup_template_file = 'sql_lookup.csh.template'; +my $sql_lookup_file = 'sql_lookup.csh'; -my $input_file4 = 'sql_lookup.cgi.template'; -my $output_file4 = 'sql_lookup.cgi'; +my $sql_lookup_cgi_template_file = 'sql_lookup.cgi.template'; +my $sql_lookup_cgi_file = 'sql_lookup.cgi'; #------------------------------------------- # Extract the version token from the path @@ -89,8 +89,8 @@ # Generate the get_aleph_info.csh script from a template. # Substitute the version token into the path of the 'source' command. #---------------------------------------------------------------------- -open(FH1,"<$input_file1") or die "Unable to open input file $input_file1\n"; -open(FH2,">$output_file1") or die "Unable to open output file $output_file1\n"; +open(FH1,"<$get_aleph_info_template_file") or die "Unable to open input file $get_aleph_info_template_file\n"; +open(FH2,">$get_aleph_info_file") or die "Unable to open output file $get_aleph_info_file\n"; while () { if (grep /source/, $_) { $_ =~ s/VER/$ver/g; @@ -99,14 +99,14 @@ } close(FH1); close(FH2); -chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file1}"); +chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${get_aleph_info_file}"); #---------------------------------------------------------- # Generate the api_adapter.cgi script from a template. # Substitute the IP addresses into the whitelist. #---------------------------------------------------------- -open(FH1,"<$input_file2") or die "Unable to open input file $input_file2\n"; -open(FH2,">$output_file2") or die "Unable to open output file $output_file2\n"; +open(FH1,"<$api_adapter_template_file") or die "Unable to open input file $api_adapter_template_file\n"; +open(FH2,">$api_adapter_file") or die "Unable to open output file $api_adapter_file\n"; while () { if (grep /WHITELIST/, $_) { $_ =~ s/WHITELIST/$ip_string/g } if (grep /PORT/, $_) { $_ =~ s/PORT/$jboss_port/og } @@ -115,7 +115,7 @@ } close(FH1); close(FH2); -chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file2}"); +chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${api_adapter_file}"); #--------------------------------------------------------------- # Optionally, generate the SQL lookup scripts from templates. @@ -125,21 +125,21 @@ # Generate the sql_lookup.csh script from a template. # Substitute the version token into the path of the 'source' command. #---------------------------------------------------------------------------- - open(FH1,"<$input_file3") or die "Unable to open input file $input_file3\n"; - open(FH2,">$output_file3") or die "Unable to open output file $output_file3\n"; + open(FH1,"<$sql_lookup_template_file") or die "Unable to open input file $sql_lookup_template_file\n"; + open(FH2,">$sql_lookup_file") or die "Unable to open output file $sql_lookup_file\n"; while () { if (grep /VER/, $_) { $_ =~ s/VER/$ver/g } } close(FH1); close(FH2); - chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file3}"); + chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${sql_lookup_file}"); #--------------------------------------------------------- # Generate the sql_lookup.cgi script from a template. # Substitute Oracle user id and password, Z308 prefix. #--------------------------------------------------------- - open(FH1,"<$input_file4") or die "Unable to open input file $input_file4\n"; - open(FH2,">$output_file4") or die "Unable to open output file $output_file4\n"; + open(FH1,"<$sql_lookup_cgi_template_file") or die "Unable to open input file $sql_lookup_cgi_template_file\n"; + open(FH2,">$sql_lookup_cgi_file") or die "Unable to open output file $sql_lookup_cgi_file\n"; while () { if (grep /DBUSER/, $_) { $_ =~ s/DBUSER/$db_user/g } if (grep /DBPASSWORD/, $_) { $_ =~ s/DBPASSWORD/$db_password/g } @@ -147,7 +147,7 @@ } close(FH1); close(FH2); - chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${output_file4}"); + chmod(S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH, "${sql_lookup_cgi_file}"); } #---------------------------- From 41745aa7b7280273e7ce332fb58de0ca6b625228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Sun, 8 Mar 2015 14:47:52 +0100 Subject: [PATCH 12/31] Better verification and escaping of parameters. --- api_adapter.template | 2 +- sql_lookup.cgi.template | 45 +++++++++++++++++++++++------------------ sql_lookup.csh.template | 4 ++-- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/api_adapter.template b/api_adapter.template index b143b31..b0aaafb 100644 --- a/api_adapter.template +++ b/api_adapter.template @@ -186,7 +186,7 @@ if ($group eq 'ilsinstance') { # Incoming identifer requires translation. Since $sql_lookup # is on, convert it to an Aleph id via SQL lookup. #--------------------------------------------------------------------- - $aleph_id = `./sql_lookup.csh $patron_id`; + $aleph_id = `./sql_lookup.csh '$patron_id'`; print STDERR "*** Aleph id from SQL: $aleph_id ***\n" if $debug; $request_uri =~ s/$patron_id/$aleph_id/ig; } diff --git a/sql_lookup.cgi.template b/sql_lookup.cgi.template index 2539c66..2dc5276 100644 --- a/sql_lookup.cgi.template +++ b/sql_lookup.cgi.template @@ -2,50 +2,55 @@ use strict; use warnings; -use DBI; # SQL access to Oracle +use DBI; # SQL access to Oracle use DBD::Oracle; -my $aleph_id; -my $debug = 0; -my $patron_id = uc $ARGV[0]; -if (!$patron_id) { + +unless (scalar(@ARGV) == 1 and $ARGV[0] !~ /^\s*$/) { print "NULL"; exit; } + +my $aleph_id; +my $debug = 0; +my $patron_id = uc($ARGV[0]); #-------------------------- -# Set variables +# Set variables #-------------------------- -my $db_user = 'DBUSER'; +my $db_user = 'DBUSER'; my $db_password = 'DBPASSWORD'; my $z308_prefix = 'Z308PRE'; my $oracle_host = $ENV{'ORA_HOST'}; -my $oracle_sid = $ENV{'ORACLE_SID'}; +my $oracle_sid = $ENV{'ORACLE_SID'}; #------------------------------------- -# Open connection to Oracle +# Open connection to Oracle #------------------------------------- -my $dbh = DBI->connect ("dbi:Oracle:host=$oracle_host;sid=$oracle_sid", $db_user, $db_password, - {RaiseError=> 1, AutoCommit=> 0 }) or warn "Unable to connect: $DBI::errstr"; +my $dbh = DBI->connect ("dbi:Oracle:host=$oracle_host;sid=$oracle_sid", "$db_user", "$db_password", + {RaiseError=> 1, AutoCommit=> 0 }) + or warn "Unable to connect: $DBI::errstr"; #--------------------------------------------- -# Prepare and execute SQL statement +# Prepare and execute SQL statement #--------------------------------------------- -my $search_term = join '', $z308_prefix, $patron_id, '%'; -my @sqlsearch = qq{ SELECT /*+ DYNAMIC_SAMPLING(2) ALL_ROWS */ /*+ ordered */ z308_id +my $search_term = join('', $z308_prefix, $patron_id); +my $sqlsearch = qq{ SELECT /*+ DYNAMIC_SAMPLING(2) ALL_ROWS */ /*+ ordered */ z308_id FROM z308 - WHERE z308_rec_key LIKE \'$search_term\' }; -print STDERR "@sqlsearch\n" if $debug; -my $sth = $dbh->prepare(@sqlsearch); -$sth->execute or warn "SQL execution failure: $DBI::errstr"; + WHERE z308_rec_key LIKE ? }; + +print STDERR "$sqlsearch\n" if $debug; +my $sth = $dbh->prepare($sqlsearch); +$sth->execute($search_term.'%') + or warn "SQL execution failure: $DBI::errstr"; #-------------------------------- -# Retrieve results from query +# Retrieve results from query #-------------------------------- while (my @data = $sth->fetchrow_array()) { $aleph_id = $data[0] } $aleph_id =~ s/ *$//o; print "$aleph_id"; #------------------------------------- -# Disconnect from Oracle +# Disconnect from Oracle #------------------------------------- $dbh->disconnect; diff --git a/sql_lookup.csh.template b/sql_lookup.csh.template index a314123..a4c78bf 100644 --- a/sql_lookup.csh.template +++ b/sql_lookup.csh.template @@ -1,6 +1,6 @@ #!/bin/tcsh source /exlibris/aleph/VER/alephm/.cshrc >/dev/null -set parm1 = $1 -perl sql_lookup.cgi $parm1 +set parm1 = "$1" +perl sql_lookup.cgi "$parm1" # vim:textwidth=80:expandtab:tabstop=4:shiftwidth=4:fileencodings=utf8:spelllang=en From 4703de480d1b2db467d5666a7cbe402e2863bcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Sun, 8 Mar 2015 21:59:41 +0100 Subject: [PATCH 13/31] More reliable Aleph version string detection. Use of the current user (should be the Aleph system user account) $HOME environment variable should be more reliable then use of PWD. --- README.md | 2 +- install_adapter.pl | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8483f0d..c3dde64 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ When the adapter is implemented it does not foreclose the direct use of the REST The installation script will perform the following operations: - a. Get the ‘PWD’ environment variable and extract the version token from the path. + a. Get the ‘HOME’ environment variable and extract the version token from the path. b. Prompt the installer for the institution’s name. This will be used in the adapter’s response to an ‘ilsinstance’ call, the one non-Aleph call that it accepts. Here is an example of the structure returned by the ‘ilsinstance’ call, reporting the institution’s name, the version of Aleph, and a few site-specific Aleph variables: ``` diff --git a/install_adapter.pl b/install_adapter.pl index 351a52e..1a312dd 100755 --- a/install_adapter.pl +++ b/install_adapter.pl @@ -20,9 +20,13 @@ #------------------------------------------- # Extract the version token from the path #------------------------------------------- -my $string = $ENV{PWD}; -my $ver = substr($string,16,5); -$ver =~ s/u/a/i; +my $current_user_home_dir = $ENV{HOME}; +my $ver; +if ($current_user_home_dir =~ /[au]([^\/]+?)\/alephm$/) { + $ver = "a$1"; +} else { + die "Unable to extract Aleph version from the current user \$HOME path\n"; +} #----------------------------------------------------- # Prompt the installer for the institution's name. @@ -153,7 +157,7 @@ #---------------------------- # Validate the installation #---------------------------- -print "Validating the installation\n"; +print "\nValidating the installation\n"; my @messages = `./validate.pl $aleph_id $jboss_port`; foreach (@messages) { print } exit; From 840021d5445f8f910751b130b3c92f88ab7344a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Sun, 8 Mar 2015 22:16:07 +0100 Subject: [PATCH 14/31] Fix install_adapter.pl template processing. sql_lookup.{cgi,csh} are properly generated from templates now. --- install_adapter.pl | 54 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/install_adapter.pl b/install_adapter.pl index 1a312dd..7b9065d 100755 --- a/install_adapter.pl +++ b/install_adapter.pl @@ -47,9 +47,9 @@ #---------------------------------------------------------- # Store this indicator for later reading by validate.pl. #---------------------------------------------------------- -open(FH1,">sql_lookup.txt"); -print FH1 "$xsl\n"; -close(FH1); +open(OUTPUT,">sql_lookup.txt"); +print OUTPUT "$xsl\n"; +close(OUTPUT); my $db_user; my $db_password; @@ -93,32 +93,32 @@ # Generate the get_aleph_info.csh script from a template. # Substitute the version token into the path of the 'source' command. #---------------------------------------------------------------------- -open(FH1,"<$get_aleph_info_template_file") or die "Unable to open input file $get_aleph_info_template_file\n"; -open(FH2,">$get_aleph_info_file") or die "Unable to open output file $get_aleph_info_file\n"; -while () { +open(TEMPLATE,"<$get_aleph_info_template_file") or die "Unable to open input file $get_aleph_info_template_file\n"; +open(OUTPUT,">$get_aleph_info_file") or die "Unable to open output file $get_aleph_info_file\n"; +while (