a56a2cd9fea8cdb76ccbbbf229c5cd20635d2870
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Trigger / Reactor.pm
1 package OpenILS::Application::Trigger::Reactor;
2 use strict; use warnings;
3 use Encode qw/ encode /;
4 use Template;
5 use DateTime;
6 use DateTime::Format::ISO8601;
7 use Unicode::Normalize;
8 use XML::LibXML;
9 use OpenSRF::Utils qw/:datetime/;
10 use OpenSRF::Utils::Logger qw(:logger);
11 use OpenSRF::Utils::JSON;
12 use OpenILS::Application::AppUtils;
13 use OpenILS::Utils::CStoreEditor qw/:funcs/;
14 my $U = 'OpenILS::Application::AppUtils';
15
16 sub fourty_two { return 42 }
17 sub NOOP_True  { return  1 }
18 sub NOOP_False { return  0 }
19
20
21 # To be used in two places within $_TT_helpers.  Without putting the code out
22 # here, we can't really reuse it within that structure.
23 sub get_li_attr {
24     my $name = shift or return;     # the first arg is always the name
25     my ($type, $attr) = (scalar(@_) == 1) ? (undef, $_[0]) : @_;
26     # if the next is the last, it's the attributes, otherwise type
27     # use Data::Dumper; $logger->warn("get_li_attr: " . Dumper($attr));
28     ($name and @$attr) or return;
29     my $length;
30     $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
31     foreach (@$attr) {
32         $_->attr_name eq $name or next;
33         next if $length and $length != length($_->attr_value);
34         return $_->attr_value if (! $type) or $type eq $_->attr_type;
35     }
36     return;
37 }
38
39 # helper functions inserted into the TT environment
40 my $_TT_helpers; # define first so one helper can use another
41 $_TT_helpers = {
42
43     # turns a date into something TT can understand
44     format_date => sub {
45         my $date = shift;
46         $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
47         return sprintf(
48             "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
49             $date->hour,
50             $date->minute,
51             $date->second,
52             $date->day,
53             $date->month,
54             $date->year
55         );
56     },
57
58     # escapes a string for inclusion in an XML document.  escapes &, <, and > characters
59     escape_xml => sub {
60         my $str = shift;
61         $str =~ s/&/&amp;/sog;
62         $str =~ s/</&lt;/sog;
63         $str =~ s/>/&gt;/sog;
64         return $str;
65     },
66
67     escape_json => sub {
68         my $str = shift;
69         $str =~ s/([\x{0080}-\x{fffd}])/sprintf('\u%0.4x',ord($1))/sgoe;
70         return $str;
71     },
72
73     # encode email headers in UTF-8, per RFC2231
74     escape_email_header => sub {
75         my $str = shift;
76         $str = encode("MIME-Header", $str);
77         return $str;
78     },
79
80     # strip non-ASCII characters after splitting base characters and diacritics
81     # least common denominator for EDIFACT messages using the UNOB character set
82     force_jedi_unob => sub {
83         my $str = shift;
84         $str = NFD($str);
85         $str =~ s/[\x{0080}-\x{fffd}]//g;
86         return $str;
87     },
88
89     # returns the calculated user locale
90     get_user_locale => sub { 
91         my $user_id = shift;
92         return $U->get_user_locale($user_id);
93     },
94
95     # returns the calculated copy price
96     get_copy_price => sub {
97         my $copy_id = shift;
98         return $U->get_copy_price(new_editor(xact=>1), $copy_id);
99     },
100
101     # given a copy, returns the title and author in a hash
102     get_copy_bib_basics => sub {
103         my $copy_id = shift;
104         my $copy = new_editor(xact=>1)->retrieve_asset_copy([
105             $copy_id,
106             {
107                 flesh => 2,
108                 flesh_fields => {
109                     acp => ['call_number'],
110                     acn => ['record']
111                 }
112             }
113         ]);
114         if($copy->call_number->id == -1) {
115             return {
116                 title  => $copy->dummy_title,
117                 author => $copy->dummy_author,
118             };
119         } else {
120             my $mvr = $U->record_to_mvr($copy->call_number->record);
121             return {
122                 title  => $mvr->title,
123                 author => $mvr->author
124             };
125         }
126     },
127
128     # given a call number, returns the copy location with the most copies
129     get_most_populous_location => sub {
130         my $acn_id = shift;
131
132         # FIXME - there's probably a more efficient way to do this with json_query/SQL
133         my $call_number = new_editor(xact=>1)->retrieve_asset_call_number([
134             $acn_id,
135             {
136                 flesh => 1,
137                 flesh_fields => {
138                     acn => ['copies']
139                 }
140             }
141         ]);
142         my %location_count = (); my $winning_location; my $winning_total;
143         use Data::Dumper;
144         foreach my $copy (@{$call_number->copies()}) {
145             if (! defined $location_count{ $copy->location() }) {
146                 $location_count{ $copy->location() } = 1;
147             } else {
148                 $location_count{ $copy->location() } += 1;
149             }
150             if ($location_count{ $copy->location() } > $winning_total) {
151                 $winning_total = $location_count{ $copy->location() };
152                 $winning_location = $copy->location();
153             }
154         }
155
156         my $location = new_editor(xact=>1)->retrieve_asset_copy_location([
157             $winning_location, {}
158         ]);
159         return $location;
160     },
161
162     # returns the org unit setting value
163     get_org_setting => sub {
164         my($org_id, $setting) = @_;
165         return $U->ou_ancestor_setting_value($org_id, $setting);
166     },
167
168     # This basically greps/maps out ths isbn string values, but also promotes the first isbn-13 to the
169     # front of the line (so that the EDI translator takes it as primary) if there is one.
170     get_li_isbns => sub {
171         my $attrs = shift;
172         my @isbns;
173         my $primary;
174         foreach (@$attrs) {
175             $_->attr_name eq 'isbn' or next;
176             my $val = $_->attr_value;
177             if (! $primary and length($val) == 13) {
178                 $primary = $val;
179             } else {
180                 push @isbns, $val;
181             }
182         }
183         $primary and unshift @isbns, $primary;
184         $logger->debug("get_li_isbns returning isbns: " . join(', ', @isbns));
185         return @isbns;
186     },
187
188     # helpers.get_li_attr('isbn_13', li.attributes)
189     # returns matching line item attribute, or undef
190     get_li_attr => \&get_li_attr,
191
192     get_li_attr_jedi => sub {
193         # This helper has to mangle data in at least three interesting ways.
194         #
195         # 1) We'll be receiving data that may already have some \-escaped
196         # characters.
197         #
198         # 2) We need our output to be valid JSON.
199         #
200         # 3) We need our output to yield valid and unproblematic EDI when
201         # passed through edi4r by the edi_pusher.pl script.
202
203         my $value = get_li_attr(@_);
204         if ($value) {
205             # Here we can add any number of special case transformations to
206             # avoid problems with the EDI translator (or bad JSON).
207
208             # The ? character, if in the final position of a string, breaks
209             # the translator. + or ' or : could be problematic, too.
210             if ($value =~ /[\?\+':]$/) {
211                 chop $value;
212             }
213
214             # Typical vendors dealing with EDIFACT would seem not to want
215             # any unicode characters, so trash them. Yes, they're already
216             # in the data escaped like this at this point even though we
217             # haven't JSON-escaped things yet.
218             $value =~ s/\\u[0-9a-f]{4}//g;
219
220             # What the heck, get rid of [ ] too (although I couldn't get them
221             # to cause any problems for me, problems have been reported. See
222             # LP #812593).
223             $value =~ s/[\[\]]//g;
224         }
225
226         $value = OpenSRF::Utils::JSON->perl2JSON($value);
227
228         # Existing action/trigger templates expect an unquoted string.
229         $value =~ s/^"//g;
230         chop $value;
231
232         return $value;
233     },
234
235     get_queued_bib_attr => sub {
236         my $name = shift or return;     # the first arg is always the name
237         my ($attr) = @_;
238         # use Data::Dumper; $logger->warn("get_queued_bib_attr: " . Dumper($attr));
239         ($name and @$attr) or return;
240
241         my $query = {
242             select => {'vqbrad' => ['id']},
243             from => 'vqbrad',
244             where => {code => $name}
245         };
246
247         my $def_ids = new_editor()->json_query($query);
248         @$def_ids or return;
249
250         my $length;
251         $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
252         foreach (@$attr) {
253             $_->field eq @{$def_ids}[0]->{id} or next;
254             next if $length and $length != length($_->attr_value);
255             return $_->attr_value;
256         }
257         return;
258     },
259
260     get_queued_auth_attr => sub {
261         my $name = shift or return;     # the first arg is always the name
262         my ($attr) = @_;
263         # use Data::Dumper; $logger->warn("get_queued_auth_attr: " . Dumper($attr));
264         ($name and @$attr) or return;
265
266         my $query = {
267             select => {'vqarad' => ['id']},
268             from => 'vqarad',
269             where => {code => $name}
270         };
271
272         my $def_ids = new_editor()->json_query($query);
273         @$def_ids or return;
274
275         my $length;
276         $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
277         foreach (@$attr) {
278             $_->field eq @{$def_ids}[0]->{id} or next;
279             next if $length and $length != length($_->attr_value);
280             return $_->attr_value;
281         }
282         return;
283     },
284
285     csv_datum => sub {
286         my ($str) = @_;
287
288         if ($str =~ /\,/ || $str =~ /"/) {
289             $str =~ s/"/""/g;
290             $str = '"' . $str . '"';
291         }
292
293         return $str;
294     },
295
296
297     bre_open_hold_count => sub {
298         my $bre_id = shift;
299         return 0 unless $bre_id;
300         return $U->simplereq(
301             'open-ils.circ',
302             'open-ils.circ.bre.holds.count', $bre_id);
303     },
304
305     xml_doc => sub {
306         my ($str) = @_;
307         return $str ? (new XML::LibXML)->parse_string($str) : undef;
308     },
309
310     # returns an email addresses derived from sms_carrier and sms_notify
311     get_sms_gateway_email => sub {
312         my $sms_carrier = shift;
313         my $sms_notify = shift;
314
315         if (! defined $sms_notify || $sms_notify eq '') {
316             return '';
317         }
318
319         my $query = {
320             select => {'csc' => ['id','name','email_gateway']},
321             from => 'csc',
322             where => {id => $sms_carrier}
323         };
324         my $carriers = new_editor()->json_query($query);
325
326         my @addresses = ();
327         foreach my $carrier ( @{ $carriers } ) {
328             my $address = $carrier->{email_gateway};
329             $address =~ s/\$number/$sms_notify/g;
330             push @addresses, $address;
331         }
332
333         return join(',',@addresses);
334     },
335
336     unapi_bre => sub {
337         my ($bre_id, $unapi_args) = @_;
338         $unapi_args ||= {};
339         $unapi_args->{flesh} ||= '{}',
340
341         my $query = { 
342             from => [
343                 'unapi.bre', $bre_id, 'marcxml','record', 
344                 $unapi_args->{flesh}, 
345                 $unapi_args->{site}, 
346                 $unapi_args->{depth}, 
347                 $unapi_args->{flesh_depth}, 
348             ]
349         };
350
351         my $unapi = new_editor()->json_query($query);
352         return undef unless @$unapi;
353         return $_TT_helpers->{xml_doc}->($unapi->[0]->{'unapi.bre'});
354     }
355 };
356
357
358 # processes templates.  Returns template output on success, undef on error
359 sub run_TT {
360     my $self = shift;
361     my $env = shift;
362     my $nostore = shift;
363     return undef unless $env->{template};
364
365     my $error;
366     my $output = '';
367     my $tt = Template->new;
368     # my $tt = Template->new(ENCODING => 'utf8');   # ??
369     $env->{helpers} = $_TT_helpers;
370
371     unless( $tt->process(\$env->{template}, $env, \$output) ) {
372         $output = undef;
373         ($error = $tt->error) =~ s/\n/ /og;
374         $logger->error("Error processing Trigger template: $error");
375     }
376
377     if ( $error or (!$nostore && $output) ) {
378         my $t_o = Fieldmapper::action_trigger::event_output->new;
379         $t_o->data( ($error) ? $error : $output );
380         $t_o->is_error( ($error) ? 't' : 'f' );
381         $logger->info("trigger: writing " . length($t_o->data) . " bytes to template output");
382
383         $env->{EventProcessor}->editor->xact_begin;
384         $t_o = $env->{EventProcessor}->editor->create_action_trigger_event_output( $t_o );
385
386         my $state = (ref $$env{event} eq 'ARRAY') ? $$env{event}->[0]->state : $env->{event}->state;
387         my $key = ($error) ? 'error_output' : 'template_output';
388         $env->{EventProcessor}->update_state( $state, { $key => $t_o->id } );
389     }
390         
391     return $output;
392 }
393
394
395 1;