14a69e7b31f2615b5fb4077d197a6a15222cd379
[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 = {
41
42     # turns a date into something TT can understand
43     format_date => sub {
44         my $date = shift;
45         $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
46         return sprintf(
47             "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
48             $date->hour,
49             $date->minute,
50             $date->second,
51             $date->day,
52             $date->month,
53             $date->year
54         );
55     },
56
57     # escapes a string for inclusion in an XML document.  escapes &, <, and > characters
58     escape_xml => sub {
59         my $str = shift;
60         $str =~ s/&/&amp;/sog;
61         $str =~ s/</&lt;/sog;
62         $str =~ s/>/&gt;/sog;
63         return $str;
64     },
65
66     escape_json => sub {
67         my $str = shift;
68         $str =~ s/([\x{0080}-\x{fffd}])/sprintf('\u%0.4x',ord($1))/sgoe;
69         return $str;
70     },
71
72     # encode email headers in UTF-8, per RFC2231
73     escape_email_header => sub {
74         my $str = shift;
75         $str = encode("MIME-Header", $str);
76         return $str;
77     },
78
79     # strip non-ASCII characters after splitting base characters and diacritics
80     # least common denominator for EDIFACT messages using the UNOB character set
81     force_jedi_unob => sub {
82         my $str = shift;
83         $str = NFD($str);
84         $str =~ s/[\x{0080}-\x{fffd}]//g;
85         return $str;
86     },
87
88     # returns the calculated user locale
89     get_user_locale => sub { 
90         my $user_id = shift;
91         return $U->get_user_locale($user_id);
92     },
93
94     # returns the calculated copy price
95     get_copy_price => sub {
96         my $copy_id = shift;
97         return $U->get_copy_price(new_editor(xact=>1), $copy_id);
98     },
99
100     # given a copy, returns the title and author in a hash
101     get_copy_bib_basics => sub {
102         my $copy_id = shift;
103         my $copy = new_editor(xact=>1)->retrieve_asset_copy([
104             $copy_id,
105             {
106                 flesh => 2,
107                 flesh_fields => {
108                     acp => ['call_number'],
109                     acn => ['record']
110                 }
111             }
112         ]);
113         if($copy->call_number->id == -1) {
114             return {
115                 title  => $copy->dummy_title,
116                 author => $copy->dummy_author,
117             };
118         } else {
119             my $mvr = $U->record_to_mvr($copy->call_number->record);
120             return {
121                 title  => $mvr->title,
122                 author => $mvr->author
123             };
124         }
125     },
126
127     # returns the org unit setting value
128     get_org_setting => sub {
129         my($org_id, $setting) = @_;
130         return $U->ou_ancestor_setting_value($org_id, $setting);
131     },
132
133     # This basically greps/maps out ths isbn string values, but also promotes the first isbn-13 to the
134     # front of the line (so that the EDI translator takes it as primary) if there is one.
135     get_li_isbns => sub {
136         my $attrs = shift;
137         my @isbns;
138         my $primary;
139         foreach (@$attrs) {
140             $_->attr_name eq 'isbn' or next;
141             my $val = $_->attr_value;
142             if (! $primary and length($val) == 13) {
143                 $primary = $val;
144             } else {
145                 push @isbns, $val;
146             }
147         }
148         $primary and unshift @isbns, $primary;
149         $logger->debug("get_li_isbns returning isbns: " . join(', ', @isbns));
150         return @isbns;
151     },
152
153     # helpers.get_li_attr('isbn_13', li.attributes)
154     # returns matching line item attribute, or undef
155     get_li_attr => \&get_li_attr,
156
157     get_li_attr_jedi => sub {
158         # This helper has to mangle data in at least three interesting ways.
159         #
160         # 1) We'll be receiving data that may already have some \-escaped
161         # characters.
162         #
163         # 2) We need our output to be valid JSON.
164         #
165         # 3) We need our output to yield valid and unproblematic EDI when
166         # passed through edi4r by the edi_pusher.pl script.
167
168         my $value = get_li_attr(@_);
169         if ($value) {
170             # Here we can add any number of special case transformations to
171             # avoid problems with the EDI translator (or bad JSON).
172
173             # The ? character, if in the final position of a string, breaks
174             # the translator. + or ' or : could be problematic, too.
175             if ($value =~ /[\?\+':]$/) {
176                 chop $value;
177             }
178
179             # Typical vendors dealing with EDIFACT would seem not to want
180             # any unicode characters, so trash them. Yes, they're already
181             # in the data escaped like this at this point even though we
182             # haven't JSON-escaped things yet.
183             $value =~ s/\\u[0-9a-f]{4}//g;
184
185             # What the heck, get rid of [ ] too (although I couldn't get them
186             # to cause any problems for me, problems have been reported. See
187             # LP #812593).
188             $value =~ s/[\[\]]//g;
189         }
190
191         $value = OpenSRF::Utils::JSON->perl2JSON($value);
192
193         # Existing action/trigger templates expect an unquoted string.
194         $value =~ s/^"//g;
195         chop $value;
196
197         return $value;
198     },
199
200     get_queued_bib_attr => sub {
201         my $name = shift or return;     # the first arg is always the name
202         my ($attr) = @_;
203         # use Data::Dumper; $logger->warn("get_queued_bib_attr: " . Dumper($attr));
204         ($name and @$attr) or return;
205
206         my $query = {
207             select => {'vqbrad' => ['id']},
208             from => 'vqbrad',
209             where => {code => $name}
210         };
211
212         my $def_ids = new_editor()->json_query($query);
213         @$def_ids or return;
214
215         my $length;
216         $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
217         foreach (@$attr) {
218             $_->field eq @{$def_ids}[0]->{id} or next;
219             next if $length and $length != length($_->attr_value);
220             return $_->attr_value;
221         }
222         return;
223     },
224
225     get_queued_auth_attr => sub {
226         my $name = shift or return;     # the first arg is always the name
227         my ($attr) = @_;
228         # use Data::Dumper; $logger->warn("get_queued_auth_attr: " . Dumper($attr));
229         ($name and @$attr) or return;
230
231         my $query = {
232             select => {'vqarad' => ['id']},
233             from => 'vqarad',
234             where => {code => $name}
235         };
236
237         my $def_ids = new_editor()->json_query($query);
238         @$def_ids or return;
239
240         my $length;
241         $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
242         foreach (@$attr) {
243             $_->field eq @{$def_ids}[0]->{id} or next;
244             next if $length and $length != length($_->attr_value);
245             return $_->attr_value;
246         }
247         return;
248     },
249
250     csv_datum => sub {
251         my ($str) = @_;
252
253         if ($str =~ /\,/ || $str =~ /"/) {
254             $str =~ s/"/""/g;
255             $str = '"' . $str . '"';
256         }
257
258         return $str;
259     },
260
261
262     bre_open_hold_count => sub {
263         my $bre_id = shift;
264         return 0 unless $bre_id;
265         return $U->simplereq(
266             'open-ils.circ',
267             'open-ils.circ.bre.holds.count', $bre_id);
268     },
269
270     xml_doc => sub {
271         my ($str) = @_;
272         return $str ? (new XML::LibXML)->parse_string($str) : undef;
273     }
274
275 };
276
277
278 # processes templates.  Returns template output on success, undef on error
279 sub run_TT {
280     my $self = shift;
281     my $env = shift;
282     my $nostore = shift;
283     return undef unless $env->{template};
284
285     my $error;
286     my $output = '';
287     my $tt = Template->new;
288     # my $tt = Template->new(ENCODING => 'utf8');   # ??
289     $env->{helpers} = $_TT_helpers;
290
291     unless( $tt->process(\$env->{template}, $env, \$output) ) {
292         $output = undef;
293         ($error = $tt->error) =~ s/\n/ /og;
294         $logger->error("Error processing Trigger template: $error");
295     }
296
297     if ( $error or (!$nostore && $output) ) {
298         my $t_o = Fieldmapper::action_trigger::event_output->new;
299         $t_o->data( ($error) ? $error : $output );
300         $t_o->is_error( ($error) ? 't' : 'f' );
301         $logger->info("trigger: writing " . length($t_o->data) . " bytes to template output");
302
303         $env->{EventProcessor}->editor->xact_begin;
304         $t_o = $env->{EventProcessor}->editor->create_action_trigger_event_output( $t_o );
305
306         my $state = (ref $$env{event} eq 'ARRAY') ? $$env{event}->[0]->state : $env->{event}->state;
307         my $key = ($error) ? 'error_output' : 'template_output';
308         $env->{EventProcessor}->update_state( $state, { $key => $t_o->id } );
309     }
310         
311     return $output;
312 }
313
314
315 1;