LP#1860703: Create A/T hook and reactor for push integration
authorMike Rylander <mrylander@gmail.com>
Mon, 28 Oct 2019 15:03:58 +0000 (11:03 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Tue, 8 Sep 2020 14:52:40 +0000 (10:52 -0400)
There is currently no stock mechanism for pushing information out of Evergreen
to trigger activities in external systems.  Third party discovery systems,
among other external systems, would benefit from the ability to be alerted of
changes to data within an Evergreen instance.

This commit adds such a capability by supplying a new A/T reactor module that
can make HTTP requests that supply data to a third party endpoint.

To support the discovery system use case, this commit also adds a new A/T hook,
bre.edit, fired whenever a bibliographic record is modified due to staff
interaction.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Troy Leonard <leonardt@aadl.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>

12 files changed:
Open-ILS/src/extras/install/Makefile.debian-buster
Open-ILS/src/extras/install/Makefile.debian-jessie
Open-ILS/src/extras/install/Makefile.debian-stretch
Open-ILS/src/extras/install/Makefile.fedora
Open-ILS/src/extras/install/Makefile.ubuntu-bionic
Open-ILS/src/extras/install/Makefile.ubuntu-xenial
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/CallHTTP.pm [new file with mode: 0644]
Open-ILS/src/sql/Pg/400.schema.action_trigger.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.CallHTTP_at_reactor.sql [new file with mode: 0644]

index 4723734..a561a0a 100644 (file)
@@ -74,6 +74,7 @@ export DEBS = \
        libbz2-dev\
        libparse-recdescent-perl\
        libhtml-defang-perl\
+       libconfig-general-perl\
        yaz
 
 export DEB_APACHE_MODS = \
index d30fa4d..21c906b 100644 (file)
@@ -75,6 +75,7 @@ export DEBS = \
        libbz2-dev\
        libparse-recdescent-perl\
        libhtml-defang-perl\
+       libconfig-general-perl\
        yaz
 
 export DEB_APACHE_MODS = \
index d16e1a5..e5a9ce5 100644 (file)
@@ -74,6 +74,7 @@ export DEBS = \
        libbz2-dev\
        libparse-recdescent-perl\
        libhtml-defang-perl\
+       libconfig-general-perl\
        yaz
 
 export DEB_APACHE_MODS = \
index e858f98..43ba484 100644 (file)
@@ -78,6 +78,7 @@ export CPAN_MODULES = \
        Net::Z3950::Simple2ZOOM \
        Template::Plugin::POSIX \
        SRU \
+       Config::General \
        Rose::URI
 
 export CPAN_MODULES_FORCE = \
index f1478a3..519d063 100644 (file)
@@ -71,6 +71,7 @@ export DEBS = \
        libbz2-dev\
        libparse-recdescent-perl\
        libhtml-defang-perl\
+       libconfig-general-perl\
        yaz
 
 export DEB_APACHE_MODS = \
index e8251b7..9f67c14 100644 (file)
@@ -74,6 +74,7 @@ export DEBS = \
        libbz2-dev\
        libparse-recdescent-perl\
        libhtml-defang-perl\
+       libconfig-general-perl\
        yaz
 
 export DEB_APACHE_MODS = \
index 14c797b..104cf71 100644 (file)
@@ -4288,6 +4288,8 @@ sub apply_new_li_ident_attr {
 
     $e->update_biblio_record_entry($bre) or return (undef, $e->die_event);
 
+    $U->create_events_for_hook('bre.edit', $bre, $e->requestor->ws_ou);
+
     return ($source_attr);
 }
 
index 9f413b0..4cfcfbd 100644 (file)
@@ -172,6 +172,7 @@ sub biblio_record_replace_marc  {
         $e, $recid, $newxml, $source, $fix_tcn, $oargs, $strip_grps);
 
     $e->commit unless $U->event_code($res);
+    $U->create_events_for_hook('bre.edit', $res, $e->requestor->ws_ou) unless $U->event_code($res);;
 
     return $res;
 }
@@ -208,6 +209,7 @@ sub template_overlay_biblio_record_entry {
         my $success = $e->json_query(
             { from => [ 'vandelay.template_overlay_bib_record', $template, $rid ] }
         )->[0]->{'vandelay.template_overlay_bib_record'};
+        $U->create_events_for_hook('bre.edit', $rec, $e->requestor->ws_ou);
 
         $conn->respond({ record => $rid, success => $success });
     }
@@ -284,6 +286,7 @@ sub template_overlay_container {
         if ($success eq 'f') {
             $num_failed++;
         } else {
+            $U->create_events_for_hook('bre.edit', $rec, $e->requestor->ws_ou);
             $num_succeeded++;
         }
 
@@ -374,6 +377,7 @@ sub update_biblio_record_entry {
     return $e->die_event unless $e->allowed('UPDATE_RECORD');
     $e->update_biblio_record_entry($record) or return $e->die_event;
     $e->commit;
+    $U->create_events_for_hook('bre.edit', $record, $e->requestor->ws_ou);
     return 1;
 }
 
@@ -413,6 +417,7 @@ sub undelete_biblio_record_entry {
 
     $e->update_biblio_record_entry($record) or return $e->die_event;
     $e->commit;
+    $U->create_events_for_hook('bre.edit', $record, $e->requestor->ws_ou);
     return 1;
 }
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/CallHTTP.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/CallHTTP.pm
new file mode 100644 (file)
index 0000000..c79c374
--- /dev/null
@@ -0,0 +1,149 @@
+package   OpenILS::Application::Trigger::Reactor::CallHTTP;
+use       OpenILS::Application::Trigger::Reactor;
+use base 'OpenILS::Application::Trigger::Reactor';
+
+use OpenSRF::Utils::Logger qw/:logger/;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+
+# use OpenSRF::Utils::SettingsClient;
+use LWP::UserAgent;
+use URI::Escape;
+use Config::General qw(ParseConfig);
+
+use strict;
+use warnings;
+
+sub ABOUT {
+    return <<ABOUT;
+
+The CallHTTP Reactor Module attempts to make an HTTP call, usually a GET or POST.
+
+The template should output data that can be parsed by the Config::General Perl
+module.  See: https://metacpan.org/pod/Config::General
+
+Top level settings should include the HTTP method and the url.
+
+A block called Headers can be used to supply arbitrary HTTP headers.
+
+A block called Parameters can be used to append CGI parameters to the URL, most
+useful for GET form submission.  Repeated parameters are allowed.  If this block
+is used, the URL should /not/ contain any parameters, use one or the other.
+
+A HEREDOC called "content" can be used with POST or PUT to send an arbitrary block
+of content to the remote server.
+
+If the requested URL requires Basic or Digest authentication, the template can
+include top level configuration parameters to supply a user, password, realm, and
+server/port location.
+
+A default user agent string of "EvergreenReactor/1.0" is used when sending requests.
+This can be overridden using the top level "agent" setting.
+
+Example template:
+
+method   post # Valid values are post, get, put, delete, head
+url      https://example.com/api/incoming-update
+agent    MySpecialAgent/0.1
+
+user     updater
+password uPd4t3StufF
+realm    "Secret area"
+location example.com:443
+
+<Headers>
+  Accept-Language en
+</Headers>
+
+<Parameters>
+  type bib
+  id   [% target.id %]
+</Parameters>
+
+content <<MARC
+[% target.marc %]
+MARC
+
+ABOUT
+}
+
+sub handler {
+    my $self = shift;
+    my $env  = shift;
+
+    my $HTTPcontent = $self->run_TT($env) or return;
+
+    my %request_config = ParseConfig(
+        -AutoTrue => 1,
+        -String => $HTTPcontent
+    );
+
+    return unless (keys %request_config);
+
+    my $ua = LWP::UserAgent->new;
+    $ua->agent($request_config{agent} ? $request_config{agent} : 'EvergreenReactor/1.0');
+
+    my $url = $request_config{url} or return;
+    my $method = $request_config{method} or return;
+    return unless (grep { $_ eq $method } qw/post get put delete head/);
+
+    my $user = $request_config{user};
+    my $password = $request_config{password};
+    my $realm = $request_config{realm};
+    my $location = $request_config{location};
+
+    $ua->credentials($location, $realm, $user, $password)
+        if ($user and $password and $realm and $location);
+
+    if ($request_config{Headers}) {
+        for my $h (keys %{$request_config{Headers}}) {
+            $ua->default_header( $h => $request_config{Headers}{$h} );
+        }
+    }
+
+    if ($request_config{Parameters}) {
+        $url .= '?';
+        my $first = 1;
+        for my $p (keys %{$request_config{Parameters}}) {
+            my $pvalues = $request_config{Parameters}{$p};
+            $pvalues = [$pvalues] if (!ref($pvalues));
+            for my $pv (@$pvalues) {
+                $url .= "&" unless $first;
+                $first = 0;
+                $url .= "$p=".uri_escape($pv);
+            }
+        }
+    }
+
+    my @params = ($url);
+    push( @params, Content => $request_config{content} )
+        if (grep { $_ eq $method } qw/put post/);
+
+    my $response = $ua->$method(@params);
+    my $output_field = $response->is_success ? 'async_output' : 'error_output';
+
+    my $e = new_editor(xact => 1);
+    my $eo = Fieldmapper::action_trigger::event_output->new;
+    $eo->is_error( $response->is_success ? 'f' : 't');
+    $eo->data($response->as_string);
+    $eo = $e->create_action_trigger_event_output($eo) or return $e->die_event;
+
+    my @eventids;
+    if (ref $$env{event} eq 'ARRAY') {
+        @eventids = map { $_->id} @{$$env{event}};
+    } else {
+        @eventids = ($env->{event}->id);
+    }
+
+    foreach (@eventids) {
+        my $event = $e->retrieve_action_trigger_event($_);
+        $event->$output_field($eo->id);
+        $e->update_action_trigger_event($event);
+    }
+
+    $e->commit;
+
+    return 1;
+}
+
+1;
+
index 69b365d..9388dbb 100644 (file)
@@ -57,6 +57,7 @@ INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('renewal','c
 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout.due.emergency_closing','aecc','Circulation due date was adjusted by the Emergency Closing handler');
 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold.shelf_expire.emergency_closing','aech','Hold shelf expire time was adjusted by the Emergency Closing handler');
 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('booking.due.emergency_closing','aecr','Booking reservation return date was adjusted by the Emergency Closing handler');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('bre.edit','bre','A bib record was edited');
 
 -- and much more, I'm sure
 
index 822c623..7100bea 100644 (file)
@@ -20585,3 +20585,7 @@ VALUES (
     'Grid Config: acq.search.invoices',
     'cwst', 'label')
 );
+
+INSERT INTO action_trigger.reactor (module, description) VALUES (
+    'CallHTTP', 'Push event information out to an external system via HTTP'
+);
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.CallHTTP_at_reactor.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.CallHTTP_at_reactor.sql
new file mode 100644 (file)
index 0000000..e25e281
--- /dev/null
@@ -0,0 +1,14 @@
+BEGIN;
+  
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO action_trigger.reactor (module, description) VALUES (
+    'CallHTTP', 'Push event information out to an external system via HTTP'
+);
+
+INSERT INTO action_trigger.hook (key, core_type, description, passive) VALUES (
+    'bre.edit', 'bre', 'A bib record was edited', FALSE
+);
+
+COMMIT;
+