File:  [LON-CAPA] / loncom / homework / lonr.pm
Revision 1.6: download - view: text, annotated - select for diffs
Tue Jun 23 03:01:15 2009 UTC (14 years, 10 months ago) by www
Branches: MAIN
CVS tags: bz2851, HEAD
Paul Rubin's interface to R.

Works now. However, it needs additional Perl libraries, which are currently
commented out. Also, an R-package needs to be installed.

    1: # The LearningOnline Network with CAPA
    2: # Interface routines to R CAS
    3: #
    4: # $Id: lonr.pm,v 1.6 2009/06/23 03:01:15 www Exp $
    5: #
    6: # Copyright Michigan State University Board of Trustees
    7: #
    8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    9: #
   10: # LON-CAPA is free software; you can redistribute it and/or modify
   11: # it under the terms of the GNU General Public License as published by
   12: # the Free Software Foundation; either version 2 of the License, or
   13: # (at your option) any later version.
   14: #
   15: # LON-CAPA is distributed in the hope that it will be useful,
   16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   18: # GNU General Public License for more details.
   19: #
   20: # You should have received a copy of the GNU General Public License
   21: # along with LON-CAPA; if not, write to the Free Software
   22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   23: #
   24: # /home/httpd/html/adm/gpl.txt
   25: #
   26: # http://www.lon-capa.org/
   27: #
   28:  
   29: package Apache::lonr;
   30:  
   31: use strict;
   32: use IO::Socket;
   33: use Apache::lonnet;
   34: use Apache::response();
   35: use LONCAPA;
   36: 
   37: ### You need to install the libraries below for this to work!
   38: 
   39: ###use Tie::IxHash::Easy; # autoties all subhashes to keep index order
   40: ###use Data::Dumper;  # used to output hash contents
   41: 
   42: my $errormsg='';
   43: 
   44: #
   45: # Rcroak: for use with R-error messages
   46: #
   47: sub Rcroak {
   48:    $errormsg=$_[0];
   49: }
   50: 
   51: #
   52: #
   53: # Rpeel takes a string containing serialized values from R, 
   54: # peels off the first syntactically complete unit (number, string or array),
   55: # and returns a list (first unit, remainder).
   56: #
   57: sub Rpeel {
   58:         my $x = $_[0];  # the string containing the serialized R object(s)
   59:         if ($x =~ /^((?:i|d):(.+?);)(.*)$/) {
   60:                 return ($1, $+);  # x starts with a number
   61:         }
   62:         elsif ($x =~ /^s:(\d+):/) {
   63:                 my $n = $1;  # x starts with a string of length n
   64:                 if ($x =~ /^(s:\d+:\"(.{$n})\";)(.*)$/) {
   65:                         return ($1, $+);  # x starts with a valid string
   66:                 } else {
   67:                         &Rcroak('invalid string detected');
   68:                 }
   69:         }
   70:         elsif ($x =~ /^a:/) {
   71:                 # x starts with an array -- need to find the closing brace
   72:                 my $i = index $x, '{', 0;  # position of first opening brace
   73:                 if ($i < 0) {
   74:                         &Rcroak('array with no opening brace');
   75:                 }
   76:                 my $open = 1;  # counts open braces
   77:                 my $j = index $x, '}', $i; # position of first closing brace
   78:                 $i = index $x, '{', $i + 1; # position of next opening brace (if any)
   79:                 my $pos = -1;  # position of final closing brace
   80:                 do {
   81:                         if (($i < $j) && ($i > 0)) {
   82:                                 # encounter another opening brace before next closing brace
   83:                                 $open++;
   84:                                 $i = index $x, '{', $i + 1;  # find the next opening brace
   85:                         } elsif ($j > 0) {
   86:                                 # next brace encountered is a closing brace
   87:                                 $open--;
   88:                                 $pos = $j;
   89:                                 $j = index $x, '}', $j + 1;
   90:                         } else {
   91:                                 &Rcroak('unmatched left brace');
   92:                         }
   93:                 } until ($open eq 0);
   94:                 # array runs from start to $pos
   95:                 my $a = substr $x, 0, $pos + 1;  # array
   96:                 my $b = substr $x, $pos + 1;     # remainder
   97:                 return ($a, $b);
   98:         } else {
   99:                 &Rcroak('unrecognized R value');
  100:         }
  101: }
  102: # --- end Rpeel ---
  103: 
  104: #
  105: # Rreturn accepts a string containing a serialized R object
  106: # and returns either the object's value (if it is scalar) or a reference
  107: # to a hash containing the contents of the object.  Any null keys in the hash
  108: # are replaced by 'resultNNN' where NNN is the index of the entry in the original
  109: # R array.
  110: #
  111: sub Rreturn {
  112:     my $x = $_[0];  # the string containing the serialized R object(s)
  113:     $x=~s/^\"//;
  114:     $x=~s/\"$//;
  115:     $x=~s/\\\"/\"/g;
  116:     $errormsg='';
  117:     if ($x =~ /^(?:i|d):(.+?);$/) {
  118:         return $1;  # return the value of the number
  119:     } elsif ($x =~ /^s:(\d+):\"(.*)\";$/) {
  120:         # string -- verify the length
  121:         if (length($2) eq $1) {
  122:            return $2;  # return the string
  123:         } else {
  124:            return 'mismatch in string length';
  125:         }
  126:     } elsif ($x =~ /^a:(\d+):\{(.*)\}$/) {
  127:         # array
  128:         my $dim = $1;  # array size
  129:         $x = $2;  # array contents
  130:         tie(my %h,'Tie::IxHash::Easy'); # start a hash
  131:         keys(%h) = $dim; # allocate space for the hash
  132:         my $key;
  133:         my $y;
  134:         for (my $i = 0; $i < $dim; $i++) {
  135:            ($y, $x) = &Rpeel($x);  # strip off the entry for the key
  136:            if ($y eq '') {
  137:               &Rcroak('ran out of keys');
  138:            }
  139:            $key = &Rreturn($y);
  140:            if ($key eq '') {
  141:               $key = "result$i";  # correct null key
  142:            }
  143:            ($y, $x) = &Rpeel($x);  # strip off the value
  144:            if ($y eq '') {
  145:                &Rcroak('ran out of values');
  146:            }
  147:            if ($y =~ /^a:/) {
  148:                $h{$key} = \&Rreturn($y);  # array value: store as reference
  149:            } else {
  150:                $h{$key} = &Rreturn($y);  # scalar value: store the entry in the hash
  151:            }
  152:         }
  153:         if ($errormsg) { return $errormsg; }
  154:         return \%h;  # return a reference to the hash
  155:     } else {
  156:         return 'Unrecognized output';
  157:     }
  158: }
  159: # --- end Rreturn ---
  160: 
  161: sub Rentry {
  162:     my $hash = shift;  # pointer to tied hash
  163:     my $i;
  164:     if (ref($hash) ne 'HASH') {
  165:        return 'Argument to cas_hashref_entry is not a hash!';
  166:     }
  167:     while ($i = shift) {
  168:        if (exists($hash->{$i})) {
  169:           $hash = $hash->{$i};
  170:        } else {
  171:           return undef;
  172:        }
  173:        if (ref($hash) eq 'REF') {
  174:           $hash = $$hash;  # dereference one layer
  175:        } elsif (ref($hash) ne 'HASH') {
  176:           return $hash;  # drilled down to a scalar
  177:        }
  178:     }
  179: }
  180: 
  181: sub Rarray {
  182:     my $hash = shift;  # pointer to tied hash
  183:     my $i;
  184:     if (ref($hash) ne 'HASH') {
  185:        return 'Argument to cas_hashref_array is not a hash!';
  186:     }
  187:     while ($i = shift) {
  188:        if (exists($hash->{$i})) {
  189:           $hash = $hash->{$i};
  190:        } else {
  191:           return undef;
  192:        }
  193:        if (ref($hash) eq 'REF') {
  194:           $hash = $$hash;  # dereference one layer
  195:        }
  196:     }
  197:     my @returnarray=();
  198:     foreach my $key (keys(%{$hash})) {
  199:         $returnarray[$key-1]=$$hash{$key};
  200:     }
  201:     return @returnarray;
  202: }
  203: 
  204: sub connect {
  205:    return IO::Socket::UNIX->new(Peer    => $Apache::lonnet::perlvar{'lonSockDir'}.'/rsock',
  206: 				Type    => SOCK_STREAM,
  207: 				Timeout => 10);
  208: }
  209: 
  210: sub disconnect {
  211:     my ($socket)=@_;
  212:     if ($socket) { close($socket); }
  213: }
  214: 
  215: sub rreply {
  216:     my ($socket,$cmd)=@_;
  217:     if ($socket) {
  218: 	print $socket &escape($cmd)."\n";
  219:         my $reply=<$socket>;
  220:         chomp($reply);
  221:         if ($reply=~/^Incorrect/) { $reply='Error: '.$reply; }
  222:         return &unescape($reply);
  223:     } else {
  224:         return 'Error: no connection.';
  225:     }
  226: }
  227: 
  228: sub blacklisted {
  229:     my ($cmd)=@_;
  230:     foreach my $forbidden (
  231:         'read','write','scan','save','socket','connections',
  232:         'open','close',
  233:         'plot','X11','windows','quartz',
  234:         'postscript','pdf','png','jpeg',
  235:         'dev\.list','dev\.next','dev\.prev','dev\.set',
  236:         'dev\.off','dev\.copy','dev\.print','graphics',
  237:         'library','package','source','sink','objects',
  238:         'Sys\.','unlink','file\.','on\.exit','error',
  239:         'q\(\)'
  240:      ) {
  241: 	if ($cmd=~/$forbidden/s) { return 1; }
  242:     } 
  243:     return 0;
  244: }
  245: 
  246: sub r_allowed_libraries {
  247:    return ('boot','class','cluster','datasets','KernSmooth','MASS',
  248:            'methods','mgcv','nlme','nnet','rpart','spatial',
  249:            'splines','stats','stats4','survival');
  250: }
  251: 
  252: sub r_is_allowed_library {
  253:     my ($library)=@_;
  254:     foreach my $allowed_library (&r_allowed_libraries()) {
  255:        if ($library eq $allowed_library) { return 1; }
  256:     }
  257:     return 0;
  258: }
  259: 
  260: sub runscript {
  261:     my ($socket,$fullscript,$libraries)=@_;
  262:     if (&blacklisted($fullscript)) { return 'Error: blacklisted'; }
  263:     my $reply;
  264:     $fullscript=~s/[\n\r\l]//gs;
  265:     if ($libraries) {
  266:        foreach my $library (split(/\s*\,\s*/,$libraries)) {
  267:           unless ($library=~/\w/) { next; }
  268:           if (&r_is_allowed_library($library)) {
  269:               $reply=&rreply($socket,'library('.$library.');'."\n");
  270:               if ($reply=~/^Error\:/) { return $reply; }
  271:           } else { 
  272:              return 'Error: blacklisted'; 
  273:           }
  274:        }
  275:     }
  276:     foreach my $line (split(/\;/s,$fullscript)) {
  277: 	if ($line=~/\w/) { $reply=&rreply($socket,$line.";\n"); }
  278: 	if ($reply=~/^Error\:/) { return $reply; }
  279:     }
  280:     $reply=~s/^\s*//gs;
  281:     $reply=~s/\s*$//gs;
  282:     &Apache::lonxml::debug("r $fullscript \n reply $reply");
  283:     return $reply;
  284: }
  285: 
  286: sub runserializedscript {
  287:     my ($socket,$fullscript,$libraries)=@_;
  288:     if (&blacklisted($fullscript)) { return 'Error: blacklisted'; }
  289:     my $reply;
  290:     $fullscript=~s/[\n\r\l]//gs;
  291:     if ($libraries) {
  292:        foreach my $library (split(/\s*\,\s*/,$libraries)) {
  293:           unless ($library=~/\w/) { next; }
  294:           if (&r_is_allowed_library($library)) {
  295:               $reply=&rreply($socket,'library('.$library.');'."\n");
  296:               if ($reply=~/^Error\:/) { return($reply,$reply); }
  297:           } else {
  298:              return 'Error: blacklisted';
  299:           }
  300:        }
  301:     }
  302:     my @actuallines=();
  303:     foreach my $line (split(/\;/s,$fullscript)) {
  304:         if ($line=~/\w/) { push (@actuallines,$line); }
  305:     }
  306:     for (my $i=0; $i<$#actuallines; $i++) {
  307:         $reply=&rreply($socket,$actuallines[$i].";\n");
  308:         if ($reply=~/^Error\:/) { return($reply,$reply); }
  309:     }
  310: # The last line needs to be serialized
  311:     $reply=&Rreturn(&rreply($socket,"phpSerialize($actuallines[-1]);\n"));
  312:     return($reply,&Dumper($reply));
  313: }
  314: 
  315: sub r_cas_formula_fix {
  316:    my ($expression)=@_;
  317:    return &Apache::response::implicit_multiplication($expression);
  318: }
  319: 
  320: sub r_run {
  321:     my ($script,$submission,$argument,$libraries) = @_;
  322:     my $socket=&connect();
  323:     my @submissionarray=split(/\s*\,\s*/,$submission);
  324:     for (my $i=0;$i<=$#submissionarray;$i++) {
  325:         my $n=$i+1;
  326:         my $fixedsubmission=&r_cas_formula_fix($submissionarray[$i]);
  327:         $script=~s/RESPONSE\[$n\]/$fixedsubmission/gs;
  328:     }
  329:     my @argumentarray=@{$argument};
  330:     for (my $i=0;$i<=$#argumentarray;$i++) {
  331:         my $n=$i+1;
  332:         my $fixedargument=&r_cas_formula_fix($argumentarray[$i]);
  333:         $script=~s/LONCAPALIST\[$n\]/$fixedargument/gs;
  334:     }
  335:     my $reply=&runscript($socket,$script,$libraries);
  336:     &disconnect($socket);
  337:     if ($reply=~/^\s*true\s*$/i) { return 'EXACT_ANS'; }
  338:     if ($reply=~/^\s*false\s*$/i) { return 'INCORRECT'; } 
  339:     return 'BAD_FORMULA';
  340: }
  341: 
  342: sub r_eval {
  343:     my ($script,$libraries,$hashflag) = @_;
  344:     my $socket=&connect();
  345:     my $reply;
  346:     my $dump='';
  347:     if ($hashflag) {
  348:        ($reply,$dump)=&runserializedscript($socket,$script,$libraries);
  349:     } else {
  350:        $reply=&runscript($socket,$script,$libraries);
  351:     }
  352:     &disconnect($socket);
  353:     return ($reply,$dump);
  354: }
  355: 
  356: 
  357: sub compareterms {
  358:     my ($socket,$terma,$termb)=@_;
  359:     my $difference=$terma.'-('.$termb.')';
  360:     if (&blacklisted($difference)) { return 'Error: blacklisted'; }
  361:     my $reply=&rreply($socket,$difference.';');
  362:     if ($reply=~/^\s*0\s*$/) { return 'true'; }
  363:     if ($reply=~/^Error\:/) { return $reply; }
  364:     return 'false';
  365: }
  366: 
  367: sub r_check {
  368:     my ($response,$answer,$reterror) = @_;
  369:     my $socket=&connect();
  370:     my $reply=&compareterms($socket,$response,$answer);
  371:     &disconnect($socket);
  372:     # integer to string mappings come from capaParser.h
  373:     # 1 maps to 'EXACT_ANS'
  374:     if ($reply eq 'true') { return 1; }
  375:     # 11 maps to 'BAD_FORMULA'
  376:     if ($reply=~/^Error\:/) { return 11; }
  377:     # 7 maps to 'INCORRECT'
  378:     return 7;
  379: }
  380:  
  381: 1;
  382: __END__;

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>