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