File:
[LON-CAPA] /
loncom /
homework /
lonr.pm
Revision
1.5:
download - view:
text,
annotated -
select for diffs
Fri Jun 19 14:03:19 2009 UTC (15 years, 2 months ago) by
www
Branches:
MAIN
CVS tags:
HEAD
Paul Rubin's code to unserialize R objects
- use tie::ixhash::easy commented out for now. Not sure if we actually want
that => code will be defunct
- needs better error handling, original code had 'die'
1: # The LearningOnline Network with CAPA
2: # Interface routines to R CAS
3: #
4: # $Id: lonr.pm,v 1.5 2009/06/19 14:03:19 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: ### Commented out for now: use Tie::IxHash::Easy; # autoties all subhashes to keep index order
37:
38: my $errormsg='';
39:
40: #
41: # Rcroak: for use with R-error messages
42: #
43: sub Rcroak {
44: $errormsg=$_[0];
45: }
46:
47: #
48: #
49: # Rpeel takes a string containing serialized values from R,
50: # peels off the first syntactically complete unit (number, string or array),
51: # and returns a list (first unit, remainder).
52: #
53: sub Rpeel {
54: my $x = $_[0]; # the string containing the serialized R object(s)
55: if ($x =~ /^((?:i|d):(.+?);)(.*)$/) {
56: return ($1, $+); # x starts with a number
57: }
58: elsif ($x =~ /^s:(\d+):/) {
59: my $n = $1; # x starts with a string of length n
60: if ($x =~ /^(s:\d+:\"(.{$n})\";)(.*)$/) {
61: return ($1, $+); # x starts with a valid string
62: } else {
63: &Rcroak('invalid string detected');
64: }
65: }
66: elsif ($x =~ /^a:/) {
67: # x starts with an array -- need to find the closing brace
68: my $i = index $x, '{', 0; # position of first opening brace
69: if ($i < 0) {
70: &Rcroak('array with no opening brace');
71: }
72: my $open = 1; # counts open braces
73: my $j = index $x, '}', $i; # position of first closing brace
74: $i = index $x, '{', $i + 1; # position of next opening brace (if any)
75: my $pos = -1; # position of final closing brace
76: do {
77: if (($i < $j) && ($i > 0)) {
78: # encounter another opening brace before next closing brace
79: $open++;
80: $i = index $x, '{', $i + 1; # find the next opening brace
81: } elsif ($j > 0) {
82: # next brace encountered is a closing brace
83: $open--;
84: $pos = $j;
85: $j = index $x, '}', $j + 1;
86: } else {
87: &Rcroak('unmatched left brace');
88: }
89: } until ($open eq 0);
90: # array runs from start to $pos
91: my $a = substr $x, 0, $pos + 1; # array
92: my $b = substr $x, $pos + 1; # remainder
93: return ($a, $b);
94: } else {
95: &Rcroak('unrecognized R value');
96: }
97: }
98: # --- end Rpeel ---
99:
100: #
101: # Rreturn accepts a string containing a serialized R object
102: # and returns either the object's value (if it is scalar) or a reference
103: # to a hash containing the contents of the object. Any null keys in the hash
104: # are replaced by 'capaNNN' where NNN is the index of the entry in the original
105: # R array.
106: #
107: sub Rreturn {
108: my $x = $_[0]; # the string containing the serialized R object(s)
109: $errormsg='';
110: if ($x =~ /^(?:i|d):(.+?);$/) {
111: return $1; # return the value of the number
112: } elsif ($x =~ /^s:(\d+):\"(.*)\";$/) {
113: # string -- verify the length
114: if (length($2) eq $1) {
115: return $2; # return the string
116: } else {
117: return 'mismatch in string length';
118: }
119: } elsif ($x =~ /^a:(\d+):\{(.*)\}$/) {
120: # array
121: my $dim = $1; # array size
122: $x = $2; # array contents
123: tie(my %h,'Tie::IxHash::Easy'); # start a hash
124: keys(%h) = $dim; # allocate space for the hash
125: my $key;
126: my $y;
127: for (my $i = 0; $i < $dim; $i++) {
128: ($y, $x) = &Rpeel($x); # strip off the entry for the key
129: if ($y eq '') {
130: &Rcroak('ran out of keys');
131: }
132: $key = &Rreturn($y);
133: if ($key eq '') {
134: $key = "capa$i"; # correct null key
135: }
136: ($y, $x) = &Rpeel($x); # strip off the value
137: if ($y eq '') {
138: &Rcroak('ran out of values');
139: }
140: if ($y =~ /^a:/) {
141: $h{$key} = \&Rreturn($y); # array value: store as reference
142: } else {
143: $h{$key} = &Rreturn($y); # scalar value: store the entry in the hash
144: }
145: }
146: if ($errormsg) { return $errormsg; }
147: return \%h; # return a reference to the hash
148: }
149: }
150: # --- end Rreturn ---
151:
152: #
153: # Rentry takes a list of indices and gets the entry in a hash generated by Rreturn.
154: # Call: Rentry(Rvalue, index1, index2, ...) where Rvalue is a hash returned by Rreturn.
155: # Rentry will return the first scalar value it encounters (ignoring excess indices).
156: # If an invalid key is given, Rentry returns undef.
157: #
158: sub Rentry {
159: my $hash = shift; # pointer to hash
160: my $x;
161: my $i;
162: if (ref($hash) ne 'HASH') {
163: &Rcroak('argument to Rentry is not a hash');
164: }
165: while ($i = shift) {
166: if (exists $hash->{$i}) {
167: $hash = $hash->{$i};
168: } else {
169: return undef;
170: }
171: if (ref($hash) eq 'REF') {
172: $hash = $$hash; # dereference one layer
173: } elsif (ref($hash) ne 'HASH') {
174: return $hash; # drilled down to a scalar
175: }
176: }
177: }
178: # --- end Rentry ---
179:
180:
181: sub connect {
182: return IO::Socket::UNIX->new(Peer => $Apache::lonnet::perlvar{'lonSockDir'}.'/rsock',
183: Type => SOCK_STREAM,
184: Timeout => 10);
185: }
186:
187: sub disconnect {
188: my ($socket)=@_;
189: if ($socket) { close($socket); }
190: }
191:
192: sub rreply {
193: my ($socket,$cmd)=@_;
194: if ($socket) {
195: print $socket &escape($cmd)."\n";
196: my $reply=<$socket>;
197: chomp($reply);
198: if ($reply=~/^Incorrect/) { $reply='Error: '.$reply; }
199: return &unescape($reply);
200: } else {
201: return 'Error: no connection.';
202: }
203: }
204:
205: sub blacklisted {
206: my ($cmd)=@_;
207: foreach my $forbidden (
208: 'read','write','scan','save','socket','connections',
209: 'open','close',
210: 'plot','X11','windows','quartz',
211: 'postscript','pdf','png','jpeg',
212: 'dev\.list','dev\.next','dev\.prev','dev\.set',
213: 'dev\.off','dev\.copy','dev\.print','graphics',
214: 'library','package','source','sink','objects',
215: 'Sys\.','unlink','file\.','on\.exit','error',
216: 'q\(\)'
217: ) {
218: if ($cmd=~/$forbidden/s) { return 1; }
219: }
220: return 0;
221: }
222:
223: sub r_allowed_libraries {
224: return ('boot','class','cluster','datasets','KernSmooth','MASS',
225: 'methods','mgcv','nlme','nnet','rpart','spatial',
226: 'splines','stats','stats4','survival');
227: }
228:
229: sub r_is_allowed_library {
230: my ($library)=@_;
231: foreach my $allowed_library (&r_allowed_libraries()) {
232: if ($library eq $allowed_library) { return 1; }
233: }
234: return 0;
235: }
236:
237: sub runscript {
238: my ($socket,$fullscript,$libraries)=@_;
239: if (&blacklisted($fullscript)) { return 'Error: blacklisted'; }
240: my $reply;
241: $fullscript=~s/[\n\r\l]//gs;
242: if ($libraries) {
243: foreach my $library (split(/\s*\,\s*/,$libraries)) {
244: unless ($library=~/\w/) { next; }
245: if (&r_is_allowed_library($library)) {
246: $reply=&rreply($socket,'library('.$library.');'."\n");
247: if ($reply=~/^Error\:/) { return $reply; }
248: } else {
249: return 'Error: blacklisted';
250: }
251: }
252: }
253: foreach my $line (split(/\;/s,$fullscript)) {
254: if ($line=~/\w/) { $reply=&rreply($socket,$line.";\n"); }
255: if ($reply=~/^Error\:/) { return $reply; }
256: }
257: $reply=~s/^\s*//gs;
258: $reply=~s/\s*$//gs;
259: &Apache::lonxml::debug("r $fullscript \n reply $reply");
260: return $reply;
261: }
262:
263: sub r_cas_formula_fix {
264: my ($expression)=@_;
265: return &Apache::response::implicit_multiplication($expression);
266: }
267:
268: sub r_run {
269: my ($script,$submission,$argument,$libraries) = @_;
270: my $socket=&connect();
271: my @submissionarray=split(/\s*\,\s*/,$submission);
272: for (my $i=0;$i<=$#submissionarray;$i++) {
273: my $n=$i+1;
274: my $fixedsubmission=&r_cas_formula_fix($submissionarray[$i]);
275: $script=~s/RESPONSE\[$n\]/$fixedsubmission/gs;
276: }
277: my @argumentarray=@{$argument};
278: for (my $i=0;$i<=$#argumentarray;$i++) {
279: my $n=$i+1;
280: my $fixedargument=&r_cas_formula_fix($argumentarray[$i]);
281: $script=~s/LONCAPALIST\[$n\]/$fixedargument/gs;
282: }
283: my $reply=&runscript($socket,$script,$libraries);
284: &disconnect($socket);
285: if ($reply=~/^\s*true\s*$/i) { return 'EXACT_ANS'; }
286: if ($reply=~/^\s*false\s*$/i) { return 'INCORRECT'; }
287: return 'BAD_FORMULA';
288: }
289:
290: sub r_eval {
291: my ($script,$libraries) = @_;
292: my $socket=&connect();
293: my $reply=&runscript($socket,$script,$libraries);
294: &disconnect($socket);
295: return $reply;
296: }
297:
298:
299: sub compareterms {
300: my ($socket,$terma,$termb)=@_;
301: my $difference=$terma.'-('.$termb.')';
302: if (&blacklisted($difference)) { return 'Error: blacklisted'; }
303: my $reply=&rreply($socket,$difference.';');
304: if ($reply=~/^\s*0\s*$/) { return 'true'; }
305: if ($reply=~/^Error\:/) { return $reply; }
306: return 'false';
307: }
308:
309: sub r_check {
310: my ($response,$answer,$reterror) = @_;
311: my $socket=&connect();
312: my $reply=&compareterms($socket,$response,$answer);
313: &disconnect($socket);
314: # integer to string mappings come from capaParser.h
315: # 1 maps to 'EXACT_ANS'
316: if ($reply eq 'true') { return 1; }
317: # 11 maps to 'BAD_FORMULA'
318: if ($reply=~/^Error\:/) { return 11; }
319: # 7 maps to 'INCORRECT'
320: return 7;
321: }
322:
323: 1;
324: __END__;
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>