1 package OpenILS::Application::Trigger::Reactor;
2 use strict; use warnings;
3 use Encode qw/ encode /;
6 use DateTime::Format::ISO8601;
7 use Unicode::Normalize;
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';
16 sub fourty_two { return 42 }
17 sub NOOP_True { return 1 }
18 sub NOOP_False { return 0 }
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.
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;
30 $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
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;
39 # helper functions inserted into the TT environment
40 my $_TT_helpers; # define first so one helper can use another
43 # turns a date into something TT can understand
46 $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
48 "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
58 # escapes a string for inclusion in an XML document. escapes &, <, and > characters
61 $str =~ s/&/&/sog;
69 $str =~ s/([\x{0080}-\x{fffd}])/sprintf('\u%0.4x',ord($1))/sgoe;
73 # encode email headers in UTF-8, per RFC2231
74 # now a no-op as we automatically encode the headers in the SendEmail
75 # reactor, but we need to leave this here to avoid breaking templates
76 # that might have once used it
77 escape_email_header => sub {
82 # strip non-ASCII characters after splitting base characters and diacritics
83 # least common denominator for EDIFACT messages using the UNOB character set
84 force_jedi_unob => sub {
87 $str =~ s/[\x{0080}-\x{fffd}]//g;
91 # returns the calculated user locale
92 get_user_locale => sub {
94 return $U->get_user_locale($user_id);
97 # returns the calculated copy price
98 get_copy_price => sub {
100 return $U->get_copy_price(new_editor(xact=>1), $copy_id);
103 get_org_unit => sub {
105 return $org_id if ref $org_id;
106 return new_editor()->retrieve_actor_org_unit($org_id);
109 get_org_unit_ancestor_at_depth => sub {
112 $org_id = $org_id->id if ref $org_id;
113 return new_editor()->retrieve_actor_org_unit($U->org_unit_ancestor_at_depth($org_id, $depth));
116 # given a copy, returns the title and author in a hash
117 get_copy_bib_basics => sub {
119 my $copy = new_editor(xact=>1)->retrieve_asset_copy([
124 acp => ['call_number'],
129 if($copy->call_number->id == -1) {
131 title => $copy->dummy_title,
132 author => $copy->dummy_author,
135 my $mvr = $U->record_to_mvr($copy->call_number->record);
137 title => $mvr->title,
138 author => $mvr->author
143 # given a call number, returns the copy location with the most copies
144 get_most_populous_location => sub {
147 # FIXME - there's probably a more efficient way to do this with json_query/SQL
148 my $call_number = new_editor(xact=>1)->retrieve_asset_call_number([
157 my %location_count = (); my $winning_location; my $winning_total;
159 foreach my $copy (@{$call_number->copies()}) {
160 if (! defined $location_count{ $copy->location() }) {
161 $location_count{ $copy->location() } = 1;
163 $location_count{ $copy->location() } += 1;
165 if ($location_count{ $copy->location() } > $winning_total) {
166 $winning_total = $location_count{ $copy->location() };
167 $winning_location = $copy->location();
171 my $location = new_editor(xact=>1)->retrieve_asset_copy_location([
172 $winning_location, {}
177 # returns the org unit setting value
178 get_org_setting => sub {
179 my($org_id, $setting) = @_;
180 return $U->ou_ancestor_setting_value($org_id, $setting);
183 get_user_setting => sub {
184 my ($user_id, $setting) = @_;
185 my $val = new_editor()->search_actor_user_setting(
186 {usr => $user_id, name => $setting})->[0];
187 return undef unless $val;
188 return OpenSRF::Utils::JSON->JSON2perl($val->value);
191 # This basically greps/maps out ths isbn string values, but also promotes the first isbn-13 to the
192 # front of the line (so that the EDI translator takes it as primary) if there is one.
193 get_li_isbns => sub {
198 $_->attr_name eq 'isbn' or next;
199 my $val = $_->attr_value;
200 if (! $primary and length($val) == 13) {
206 $primary and unshift @isbns, $primary;
207 $logger->debug("get_li_isbns returning isbns: " . join(', ', @isbns));
211 get_li_order_ident => sub {
214 # preferred identifier
215 my ($attr) = grep { $U->is_true($_->order_ident) } @$attrs;
216 return $attr if $attr;
218 # note we're not using get_li_attr, since we need the
219 # attr object and not just the attr value
223 $_->attr_name eq 'isbn' and
224 $_->attr_type eq 'lineitem_marc_attr_definition' and
225 length($_->attr_value) == 13
227 return $attr if $attr;
229 for my $name (qw/isbn issn upc/) {
231 $_->attr_name eq $name and
232 $_->attr_type eq 'lineitem_marc_attr_definition'
234 return $attr if $attr;
237 # any 'identifier' attr
238 return ( grep { $_->attr_name eq 'identifier' } @$attrs)[0];
241 # helpers.get_li_attr('isbn_13', li.attributes)
242 # returns matching line item attribute, or undef
243 get_li_attr => \&get_li_attr,
245 # get_li_attr_jedi() returns a JSON-encoded string without the enclosing
246 # quotes. The function also removes other characters from the string
247 # that the EDI translator doesn't like.
249 # This *always* return a string, so don't use this in conditional
250 # expressions in your templates unless you really mean to.
251 get_li_attr_jedi => sub {
252 # This helper has to mangle data in at least three interesting ways.
254 # 1) We'll be receiving data that may already have some \-escaped
257 # 2) We need our output to be valid JSON.
259 # 3) We need our output to yield valid and unproblematic EDI when
260 # passed through edi4r by the edi_pusher.pl script.
262 my $value = get_li_attr(@_);
265 no warnings 'uninitialized';
266 $value .= ""; # force to string
269 # Here we can add any number of special case transformations to
270 # avoid problems with the EDI translator (or bad JSON).
272 # Typical vendors dealing with EDIFACT (or is the problem with
273 # our EDI translator itself?) would seem not to want
274 # any characters outside the ASCII range, so trash them.
275 $value =~ s/[^[:ascii:]]//g;
277 # Remove anything somehow already JSON-escaped as a Unicode
278 # character. (even though for our part, we haven't JSON-escaped
280 $value =~ s/\\u[0-9a-f]{4}//g;
282 # What the heck, get rid of [ ] too (although I couldn't get them
283 # to cause any problems for me, problems have been reported. See
285 $value =~ s/[\[\]]//g;
287 $value = OpenSRF::Utils::JSON->perl2JSON($value);
289 # Existing action/trigger templates expect an unquoted string.
293 # The ? character, if in the final position of a string, breaks
294 # the translator. + or ' or : could be problematic, too. And we must
295 # avoid leaving a hanging \.
296 while ($value =~ /[\\\?\+':]$/) {
303 get_queued_bib_attr => sub {
304 my $name = shift or return; # the first arg is always the name
306 # use Data::Dumper; $logger->warn("get_queued_bib_attr: " . Dumper($attr));
307 ($name and @$attr) or return;
310 select => {'vqbrad' => ['id']},
312 where => {code => $name}
315 my $def_ids = new_editor()->json_query($query);
319 $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
321 $_->field eq @{$def_ids}[0]->{id} or next;
322 next if $length and $length != length($_->attr_value);
323 return $_->attr_value;
328 get_queued_auth_attr => sub {
329 my $name = shift or return; # the first arg is always the name
331 # use Data::Dumper; $logger->warn("get_queued_auth_attr: " . Dumper($attr));
332 ($name and @$attr) or return;
335 select => {'vqarad' => ['id']},
337 where => {code => $name}
340 my $def_ids = new_editor()->json_query($query);
344 $name =~ s/^(\D+)_(\d+)$/$1/ and $length = $2;
346 $_->field eq @{$def_ids}[0]->{id} or next;
347 next if $length and $length != length($_->attr_value);
348 return $_->attr_value;
356 if ($str =~ /\,/ || $str =~ /"/) {
358 $str = '"' . $str . '"';
365 bre_open_hold_count => sub {
367 return 0 unless $bre_id;
368 return $U->simplereq(
370 'open-ils.circ.bre.holds.count', $bre_id);
375 return $str ? (new XML::LibXML)->parse_string($str) : undef;
378 # returns an email addresses derived from sms_carrier and sms_notify
379 get_sms_gateway_email => sub {
380 my $sms_carrier = shift;
381 my $sms_notify = shift;
383 if (! defined $sms_notify || $sms_notify eq '') {
388 select => {'csc' => ['id','name','email_gateway']},
390 where => {id => $sms_carrier}
392 my $carriers = new_editor()->json_query($query);
394 # If this looks like a pretty-formatted number drop the pretty-formatting
395 # Otherwise assume it may be a literal alias instead of a real number
396 if ($sms_notify =~ m/^[- ()0-9]*$/) {
397 $sms_notify =~ s/[- ()]//g;
401 foreach my $carrier ( @{ $carriers } ) {
402 my $address = $carrier->{email_gateway};
403 $address =~ s/\$number/$sms_notify/g;
404 push @addresses, $address;
407 return join(',',@addresses);
411 my ($bre_id, $unapi_args) = @_;
413 $unapi_args->{flesh} ||= '{}',
417 'unapi.bre', $bre_id, 'marcxml','record',
418 $unapi_args->{flesh},
420 $unapi_args->{depth},
421 $unapi_args->{flesh_depth},
425 my $unapi = new_editor()->json_query($query);
426 return undef unless @$unapi;
427 return $_TT_helpers->{xml_doc}->($unapi->[0]->{'unapi.bre'});
430 # escapes quotes in csv string values
433 $string =~ s/"/""/og;
439 # processes templates. Returns template output on success, undef on error
444 return undef unless $env->{template};
448 my $tt = Template->new;
449 # my $tt = Template->new(ENCODING => 'utf8'); # ??
450 $env->{helpers} = $_TT_helpers;
452 unless( $tt->process(\$env->{template}, $env, \$output) ) {
454 ($error = $tt->error) =~ s/\n/ /og;
455 $logger->error("Error processing Trigger template: $error");
458 if ( $error or (!$nostore && $output) ) {
459 my $t_o = Fieldmapper::action_trigger::event_output->new;
460 $t_o->data( ($error) ? $error : $output );
461 $t_o->is_error( ($error) ? 't' : 'f' );
462 $logger->info("trigger: writing " . length($t_o->data) . " bytes to template output");
464 $env->{EventProcessor}->editor->xact_begin;
465 $t_o = $env->{EventProcessor}->editor->create_action_trigger_event_output( $t_o );
467 my $state = (ref $$env{event} eq 'ARRAY') ? $$env{event}->[0]->state : $env->{event}->state;
468 my $key = ($error) ? 'error_output' : 'template_output';
469 $env->{EventProcessor}->update_state( $state, { $key => $t_o->id } );
475 # processes message templates. Returns template output on success, undef on error
479 return undef unless $env->{usr_message}{template};
483 my $tt = Template->new;
484 # my $tt = Template->new(ENCODING => 'utf8'); # ??
485 $env->{helpers} = $_TT_helpers;
487 unless( $tt->process(\$env->{usr_message}{template}, $env, \$output) ) {
489 ($error = $tt->error) =~ s/\n/ /og;
490 $logger->error("Error processing Trigger message template: $error");