]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Utils/ScriptRunner.pm
fix for mem leak in load_lib()
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Utils / ScriptRunner.pm
1 package OpenILS::Utils::ScriptRunner;
2 use strict; use warnings;
3 use OpenSRF::Utils::Logger qw(:logger);
4 use OpenSRF::EX qw(:try);
5 use JSON;
6 use JavaScript::SpiderMonkey;
7 use LWP::UserAgent;
8 use XML::LibXML;
9 use Time::HiRes qw/time/;
10 use vars qw/%_paths $__json_js/;
11
12 { local $/ = undef; $__json_js = <DATA>; }
13
14 sub new {
15         my $class = shift;
16         my %params = @_;
17         $class = ref($class) || $class;
18         $params{paths} ||= [];
19         $params{reset_count} ||= 0;
20
21         my $self = bless {      file => $params{file},
22                                 libs => $params{libs},
23                                 reset_count => $params{reset_count},
24                                 _runs => 0,
25                                 _path => {%_paths} } => $class;
26
27         $self->add_path($_) for @{$params{paths}};
28         return $self->init; 
29 }
30
31 sub context {
32         my( $self, $context ) = @_;
33         $self->{ctx} = $context if $context;
34         return $self->{ctx};
35 }
36
37 sub init {
38         my $self = shift;
39         $self->context( new JavaScript::SpiderMonkey );
40         $self->context->init();
41
42         $self->{_runs} = 0;
43
44         # eating our own dog food with insert
45         $self->insert(log_stdout        => sub { print "@_\n"; } );
46         $self->insert(log_stderr        => sub { warn "@_\n"; } );
47         $self->insert(log_activity      => sub { $logger->activity("script_runner: @_"); return 1;} );
48         $self->insert(log_error         => sub { $logger->error("script_runner: @_"); return 1;} );
49         $self->insert(log_warn          => sub { $logger->warn("script_runner: @_"); return 1;} );
50         $self->insert(log_info          => sub { $logger->info("script_runner: @_"); return 1;} );
51         $self->insert(log_debug         => sub { $logger->debug("script_runner: @_"); return 1;} );
52         $self->insert(log_internal      => sub { $logger->internal("script_runner: @_"); return 1;} );
53         $self->insert(debug             => sub { $logger->debug("script_runner: @_"); return 1;} );
54         $self->insert(alert             => sub { $logger->warn("script_runner: @_"); return 1;} );
55         $self->insert(load_lib          => sub { $self->load_lib(@_); return 1;});
56
57         # OpenSRF support functions
58         $self->insert(
59                 _OILS_FUNC_jsonopensrfrequest_send =>
60                         sub { $self->_jsonopensrfrequest_send(@_); }
61         );
62         $self->insert(
63                 _OILS_FUNC_jsonopensrfrequest_connect =>
64                         sub { $self->_jsonopensrfrequest_connect(@_); }
65         );
66         $self->insert(
67                 _OILS_FUNC_jsonopensrfrequest_disconnect =>
68                         sub { $self->_jsonopensrfrequest_disconnect(@_); }
69         );
70         $self->insert(
71                 _OILS_FUNC_jsonopensrfrequest_finish =>
72                         sub { $self->_jsonopensrfrequest_finish(@_); }
73         );
74
75         # XML support functions
76         $self->insert(
77                 _OILS_FUNC_xmlhttprequest_send  =>
78                         sub { $self->_xmlhttprequest_send(@_); }
79         );
80         $self->insert(
81                 _OILS_FUNC_xml_parse_string     =>
82                         sub { $self->_parse_xml_string(@_); }
83         );
84         
85         $self->load_lib($_) for @{$self->{libs}};
86
87         return $self;
88 }
89
90 sub refresh_context {
91         my $self = shift;
92         $self->context->destroy;
93         $self->{_loaded} = {};
94         $self->init;
95 }
96
97 sub load {
98         my( $self, $filename ) = @_;
99         $self->{file} = $filename;
100 }
101
102 sub runs { shift()->{_runs} }
103
104 sub reset_count {
105         my $self = shift;
106         my $count = shift;
107
108         $self->{reset_count} = $count if ($count);
109         return $self->{reset_count};
110 }
111
112 sub run {
113         my $self = shift;
114         my $file = shift();
115
116         my $_real = 0;
117         if(!$file) {
118                 $_real = 1;
119                 $file = $self->{file};
120         }
121
122         my $js = $self->context;
123
124
125
126         $self->refresh_context
127                 if ($self->reset_count && $self->runs > $self->reset_count);
128
129         $self->{_runs}++;
130
131         $file = $self->_find_file($file);
132         $logger->debug("full script file path: $file");
133
134         if( ! open(F, $file) ) {
135                 $logger->error("Error opening script file: $file");
136                 return 0;
137         }
138
139         my $res = '';
140         {       local $/ = undef;
141
142                 $self->insert('environment.result' => {});
143
144                 my $content = <F>;
145                 #print ( "full script is [$content]" );
146
147                 my $s = time();
148                 if( !$js || !$content || !$js->eval($content) ) {
149                         $logger->error("$file Eval failed: $@");  
150                         return 0;
151                 }
152                 $logger->debug("eval of $file took ". sprintf('%0.3f', time - $s) . " seconds");
153
154                 if ($_real) {
155                         $self->insert('__' => {'OILS_RESULT' => ''});
156                         $js->eval($__json_js."__.OILS_RESULT = js2JSON(environment.result);");
157                         $res = $self->retrieve('__.OILS_RESULT');
158                 }
159         }
160
161         close(F);
162         $logger->debug( "script result is [$res]" );
163         return JSON->JSON2perl( $res );
164 }
165
166 sub remove_path { 
167         my( $self, $path ) = @_;
168         if (ref($self)) {
169                 if ($self->{_path}{$path}) {
170                         $self->{_path}{$path} = 0;
171                 }
172                 return $self->{_path}{$path};
173         } else {
174                 if ($_paths{$path}) {
175                         $_paths{$path} = 0;
176                 }
177                 return $_paths{$path};
178         }
179 }
180
181 sub add_path { 
182         my( $self, $path ) = @_;
183         if (ref($self)) {
184                 if (!$self->{_path}{$path}) {
185                         $self->{_path}{$path} = 1;
186                 }
187         } else {
188                 if (!$_paths{$path}) {
189                         $_paths{$path} = 1;
190                 }
191         }
192         return $self;
193 }
194
195 sub _find_file {
196         my $self = shift;
197         my $file = shift;
198         for my $p ( keys %{ $self->{_path} } ) {
199                 next unless ($self->{_path}{$p});
200                 my $full = join('/',$p,$file);
201                 return $full if (-e $full);
202         }
203 }
204
205 sub load_lib { 
206         my( $self, $file ) = @_;
207
208         push @{ $self->{libs} }, $file
209                 if (! grep {$_ eq $file} @{ $self->{libs} });
210
211         if (!$self->{_loaded}{$file}) {
212                 $self->run( $file );
213                 $self->{_loaded}{$file} = 1;
214         }
215         return $self->{_loaded}{$file};
216 }
217
218 sub _js_prop_name {
219         my $name = shift;
220         $name =~ s/^.*\.//o;
221         return $name;
222 }
223
224 sub retrieve {
225         my( $self, $key ) = @_;
226         return $self->context->property_get($key);
227 }
228
229 sub insert_method {
230         my( $self, $obj_key, $meth_name, $sub ) = @_;
231         my $obj = $self->context->object_by_path( $obj_key );
232         $self->context->function_set( $meth_name, $sub, $obj ) if $obj;
233 }
234
235
236 sub insert {
237         my( $self, $key, $val, $RO ) = @_;
238         return unless defined($key);
239
240         if (ref($val) =~ /^Fieldmapper/o) {
241                 $self->insert_fm($key, $val, $RO);
242         } elsif (ref($val) and $val =~ /ARRAY/o) {
243                 $self->insert_array($key, $val, $RO);
244         } elsif (ref($val) and $val =~ /HASH/o) {
245                 $self->insert_hash($key, $val, $RO);
246         } elsif (ref($val) and $val =~ /CODE/o) {
247                 $self->context->function_set( $key, $val );
248         } elsif (!ref($val)) {
249                 if( defined($val) ) {
250                         $self->context->property_by_path(
251                                 $key, $val,
252                                 sub { $val },
253                                 ( !$RO ?
254                                         sub { my( $k, $v ) = @_; $val = $v; } :
255                                         sub{}
256                                 )
257                         );
258                 } else {
259                         $self->context->property_by_path($key, "");
260                 }
261
262         } else {
263                 return 0;
264         }
265
266         return 1;
267 }
268
269 sub insert_fm {
270
271         my( $self, $key, $fm, $RO ) = @_;
272         my $ctx = $self->context;
273         return undef unless ($ctx and $key and $fm);
274         my $o = $ctx->object_by_path($key);
275         
276         for my $f ( $fm->properties ) {
277                 my $val = $fm->$f();
278                 if (ref $val) {
279                         $self->insert("$key.$f", $val);
280                 } else {
281                         $ctx->property_by_path(
282                                 "$key.$f",
283                                 $val,
284                                 sub {
285                                         my $k = _js_prop_name(shift());
286                                         $fm->$k();
287                                 }, 
288
289                                 ( !$RO ? 
290                                         sub {
291                                                 my $k = _js_prop_name(shift());
292                                                 $fm->ischanged(1);
293                                                 $fm->$k(@_);
294                                         } :
295                                         sub {}
296                                 )
297                         );
298                 }
299         }
300 }
301
302 sub insert_hash {
303
304         my( $self, $key, $hash, $RO ) = @_;
305         my $ctx = $self->context;
306         return undef unless ($ctx and $key and $hash);
307         $ctx->object_by_path($key);
308         
309         for my $k ( keys %$hash ) {
310                 my $v = $hash->{$k};
311                 if (ref $v) {
312                         $self->insert("$key.$k", $v);
313                 } else {
314                         $ctx->property_by_path(
315                                 "$key.$k", $v,
316                                 sub { $hash->{_js_prop_name(shift())} },
317                                 ( !$RO ? 
318                                         sub {
319                                                 my( $hashkey, $val ) = @_;
320                                                 $hash->{_js_prop_name($hashkey)} = $val;
321                                         } :
322                                         sub {}
323                                 )
324                         );
325                 }
326         }
327 }
328
329 my $__array_id = 0;
330 sub insert_array {
331
332         my( $self, $key, $array ) = @_;
333         my $ctx = $self->context;
334         return undef unless ($ctx and $key and $array);
335
336         my $a = $ctx->array_by_path($key);
337         
338         my $ind = 0;
339         for my $v ( @$array ) {
340                 if (ref $v) {
341                         my $tmp_index = $__array_id++;
342                         my $elobj = $ctx->object_by_path('__tmp_arr_el'.$tmp_index);
343                         $self->insert('__tmp_arr_el'.$tmp_index, $v);
344                         $ctx->array_set_element_as_object( $a, $ind, $elobj );
345                 } else {
346                         $ctx->array_set_element( $a, $ind, $v ) if defined($v);
347                 }
348                 $ind++;
349         }
350 }
351
352 sub _xmlhttprequest_send {
353         my $self = shift;
354         my $id = shift;
355         my $method = shift;
356         my $url = shift;
357         my $blocking = shift;
358         my $headerlist = shift;
359         my $data = shift;
360
361         my $ctx = $self->context;
362
363         # just so perl has access to it...
364         $ctx->object_by_path('__xmlhttpreq_hash.id'.$id);
365
366         my $headers = new HTTP::Headers;
367         my @lines = split(/\n/so, $headerlist);
368         for my $line (@lines) {
369                 if ($line =~ /^(.+?)|(.+)$/o) {
370                         $headers->header($1 => $2);
371                 }
372         }
373
374         my $ua = LWP::UserAgent->new;
375         $ua->agent("OpenILS/0.1");
376
377         my $req = HTTP::Request->new($method => $url => $headers => $data);
378         my $res = $ua->request($req);
379
380         if ($res->is_success) {
381                 
382                 $ctx->property_by_path('__xmlhttpreq_hash.id'.$id.'.responseText', $res->content);
383                 $ctx->property_by_path('__xmlhttpreq_hash.id'.$id.'.readyState', 4);
384                 $ctx->property_by_path('__xmlhttpreq_hash.id'.$id.'.statusText', $res->status_line);
385                 $ctx->property_by_path('__xmlhttpreq_hash.id'.$id.'.status', $res->code);
386
387         }
388                 
389 }
390
391 our %_jsonopensrfrequest_cache = ();
392
393 sub _jsonopensrfrequest_connect {
394         my $self = shift;
395         my $id = shift;
396         my $service = shift;
397
398         my $ctx = $self->context;
399         $ctx->object_by_path('__jsonopensrfreq_hash.id'.$id);
400
401         my $ses = $_jsonopensrfrequest_cache{$id} ||
402                         do { $_jsonopensrfrequest_cache{$id} = OpenSRF::AppSession->create($service) };
403
404         if($ses->connect) {
405                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.connected', 1);
406         } else {
407                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.connected', 0);
408         }
409 }
410
411 sub _jsonopensrfrequest_disconnect {
412         my $self = shift;
413         my $id = shift;
414
415         my $ctx = $self->context;
416         $ctx->object_by_path('__jsonopensrfreq_hash.id'.$id);
417
418         my $ses = $_jsonopensrfrequest_cache{$id};
419         return unless $ses;
420
421         $ses->disconnect;
422 }
423
424 sub _jsonopensrfrequest_finish {
425         my $self = shift;
426         my $id = shift;
427
428         my $ctx = $self->context;
429         $ctx->object_by_path('__jsonopensrfreq_hash.id'.$id);
430
431         my $ses = $_jsonopensrfrequest_cache{$id};
432         return unless $ses;
433
434         $ses->finish;
435         delete $_jsonopensrfrequest_cache{$id};
436 }
437
438 sub _jsonopensrfrequest_send {
439         my $self = shift;
440         my $id = shift;
441         my $service = shift;
442         my $method = shift;
443         my $blocking = shift;
444         my $params = shift;
445
446         my @p = @{ JSON->JSON2perl($params) };
447
448         my $ctx = $self->context;
449
450         # just so perl has access to it...
451         $ctx->object_by_path('__jsonopensrfreq_hash.id'.$id);
452
453         my $ses = $_jsonopensrfrequest_cache{$id} ||
454                         do { $_jsonopensrfrequest_cache{$id} = OpenSRF::AppSession->create($service) };
455         my $req = $ses->request($method,@p);
456
457         $req->wait_complete;
458         if (!$req->failed) {
459                 my $res = $req->recv->content;
460                 
461                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.responseText', JSON->perl2JSON($res));
462                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.readyState', 4);
463                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.statusText', 'OK');
464                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.status', '200');
465
466         } else {
467                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.responseText', '');
468                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.readyState', 4);
469                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.statusText', $req->failed->status );
470                 $ctx->property_by_path('__jsonopensrfreq_hash.id'.$id.'.status', $req->failed->statusCode );
471         }
472
473         $req->finish;
474                 
475 }
476
477 sub _parse_xml_string {
478         my $self = shift;
479         my $string = shift;
480         my $key = shift;
481
482
483         my $doc;
484         my $s = 0;
485         try {
486                 $doc = XML::LibXML->new->parse_string( $string );
487                 $s = 1;
488         } catch Error with {
489                 my $e = shift;
490                 warn "Could not parse document: $e\n";
491         };
492         return unless ($s);
493
494         _JS_DOM($self->context, $key, $doc);
495 }
496
497 sub _JS_DOM {
498         my $ctx = shift;
499         my $key = shift;
500         my $node = shift;
501
502         if ($node->nodeType == 9) {
503                 $node = $node->documentElement;
504
505                 my $n = $node->nodeName;
506                 my $ns = $node->namespaceURI;
507                 $ns =~ s/'/\'/gso if ($ns);
508                 $ns = "'$ns'" if ($ns);
509                 $ns = 'null' unless ($ns);
510                 $n =~ s/'/\'/gso;
511
512                 #warn("$key = DOMImplementation().createDocument($ns,'$n');");
513                 $ctx->eval("$key = new DOMImplementation().createDocument($ns,'$n');");
514
515                 $key = $key.'.documentElement';
516         }
517
518         for my $a ($node->attributes) {
519                 my $n = $a->nodeName;
520                 my $v = $a->value;
521                 $n =~ s/'/\'/gso;
522                 $v =~ s/'/\'/gso;
523                 #warn("$key.setAttribute('$n','$v');");
524                 $ctx->eval("$key.setAttribute('$n','$v');");
525
526         }
527
528         my $k = 0;
529         for my $c ($node->childNodes) {
530                 if ($c->nodeType == 1) {
531                         my $n = $c->nodeName;
532                         my $ns = $node->namespaceURI;
533
534                         $n =~ s/'/\'/gso;
535                         $ns =~ s/'/\'/gso if ($ns);
536                         $ns = "'$ns'" if ($ns);
537                         $ns = 'null' unless ($ns);
538
539                         #warn("$key.appendChild($key.ownerDocument.createElementNS($ns,'$n'));");
540                         $ctx->eval("$key.appendChild($key.ownerDocument.createElementNS($ns,'$n'));");
541                         _JS_DOM($ctx, "$key.childNodes.item($k)",$c);
542
543                 } elsif ($c->nodeType == 3) {
544                         my $n = $c->data;
545                         $n =~ s/'/\'/gso;
546                         #warn("$key.appendChild($key.ownerDocument.createTextNode('$n'));");
547                         #warn("path is $key.item($k);");
548                         $ctx->eval("$key.appendChild($key.ownerDocument.createTextNode('$n'));");
549
550                 } elsif ($c->nodeType == 4) {
551                         my $n = $c->data;
552                         $n =~ s/'/\'/gso;
553                         #warn("$key.appendChild($key.ownerDocument.createCDATASection('$n'));");
554                         $ctx->eval("$key.appendChild($key.ownerDocument.createCDATASection('$n'));");
555
556                 } elsif ($c->nodeType == 8) {
557                         my $n = $c->data;
558                         $n =~ s/'/\'/gso;
559                         #warn("$key.appendChild($key.ownerDocument.createComment('$n'));");
560                         $ctx->eval("$key.appendChild($key.ownerDocument.createComment('$n'));");
561
562                 } else {
563                         warn "ACK! I don't know how to handle node type ".$c->nodeType;
564                 }
565                 
566
567                 $k++;
568         }
569
570         return 1;
571 }
572
573
574
575 1;
576
577 __DATA__
578
579 // in case we run on an implimentation that doesn't have "undefined";
580 var undefined;
581
582 function Cast (obj, class_constructor) {
583         try {
584                 if (eval(class_constructor + '["_isfieldmapper"]')) {
585                         obj = eval("new " + class_constructor + "(obj)");
586                 }
587         } catch( E ) {
588                 alert( E + "\n");
589         } finally {
590                 return obj;
591         }
592 }
593
594 function JSON2js (json) {
595
596         json = String(json).replace( /\/\*--\s*S\w*?\s*?\s+\w+\s*--\*\//g, 'Cast(');
597         json = String(json).replace( /\/\*--\s*E\w*?\s*?\s+(\w+)\s*--\*\//g, ', "$1")');
598
599         var obj;
600         if (json != '') {
601                 try {
602                         eval( 'obj = ' + json );
603                 } catch(E) {
604                         debug("Error building JSON object with string " + E + "\nString:\n" + json );
605                         return null;
606                 }
607         }
608         return obj;
609 }
610
611
612 function object2Array(obj) {
613         if( obj == null ) return null;
614
615         var arr = new Array();
616         for( var i  = 0; i < obj.length; i++ ) {
617                 arr[i] = obj[i];
618         }
619         return arr;
620 }
621
622
623 function js2JSON(arg) {
624         return _js2JSON(arg);
625 }
626
627 function _js2JSON(arg) {
628         var i, o, u, v;
629
630                 switch (typeof arg) {
631                         case 'object':
632         
633                                 if(arg) {
634         
635                                         if (arg._isfieldmapper) { /* magi-c-ast for fieldmapper objects */
636         
637                                                 if( arg.a.constructor != Array ) {
638                                                         var arr = new Array();
639                                                         for( var i  = 0; i < arg.a.length; i++ ) {
640                                                                 if( arg.a[i] == null ) {
641                                                                         arr[i] = null; continue;
642                                                                 }
643         
644                                                                 if( typeof arg.a[i] != 'object' ) { 
645                                                                         arr[i] = arg.a[i];
646         
647                                                                 } else if( typeof arg.a[i] == 'object' 
648                                                                                         && arg.a[i]._isfieldmapper) {
649         
650                                                                         arr[i] = arg.a[i];
651         
652                                                                 } else {
653                                                                         arr[i] = object2Array(arg.a[i]);                
654                                                                 }
655                                                         }
656                                                         arg.a = arr;
657                                                 }
658         
659                                                 return "/*--S " + arg.classname + " --*/" + js2JSON(arg.a) + "/*--E " + arg.classname + " --*/";
660         
661                                         } else {
662         
663                                                 if (arg.constructor == Array) {
664                                                         o = '';
665                                                         for (i = 0; i < arg.length; ++i) {
666                                                                 v = js2JSON(arg[i]);
667                                                                 if (o) {
668                                                                         o += ',';
669                                                                 }
670                                                                 if (v !== u) {
671                                                                         o += v;
672                                                                 } else {
673                                                                         o += 'null';
674                                                                 }
675                                                         }
676                                                         return '[' + o + ']';
677         
678                                                 } else if (typeof arg.toString != 'undefined') {
679                                                         o = '';
680                                                         for (i in arg) {
681                                                                 v = js2JSON(arg[i]);
682                                                                 if (v !== u) {
683                                                                         if (o) {
684                                                                                 o += ',';
685                                                                         }
686                                                                         o += js2JSON(i) + ':' + v;
687                                                                 }
688                                                         }
689         
690                                                         o = '{' + o + '}';
691                                                         return o;
692         
693                                                 } else {
694                                                         return;
695                                                 }
696                                         }
697                                 }
698                                 return 'null';
699         
700                         case 'unknown':
701                         case 'number':
702                                 if( isNaN(arg) ) throw "JSON.js encountered NaN in js2JSON()";
703                                 return arg;
704         
705                         case 'undefined':
706                         case 'function':
707                                 return u;
708         
709                         case 'string':
710                         default:
711                                 return '"' + String(arg).replace(/(["\\])/g, '\\$1') + '"';
712                 }
713
714 }
715