View Javadoc
1   /*
2    * The baseCode project
3    *
4    * Copyright (c) 2006 University of British Columbia
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *       http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   *
18   */
19  package ubic.basecode.util.r;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.URL;
24  import java.util.Iterator;
25  import java.util.List;
26  
27  import org.apache.commons.configuration2.PropertiesConfiguration;
28  import org.apache.commons.configuration2.ex.ConfigurationException;
29  import org.apache.commons.configuration2.io.FileHandler;
30  import org.apache.commons.lang3.StringUtils;
31  import org.rosuda.REngine.REXP;
32  import org.rosuda.REngine.REXPMismatchException;
33  import org.rosuda.REngine.REngineException;
34  import org.rosuda.REngine.RList;
35  import org.rosuda.REngine.Rserve.RConnection;
36  import org.rosuda.REngine.Rserve.RserveException;
37  
38  import ubic.basecode.dataStructure.matrix.DenseDoubleMatrix;
39  import ubic.basecode.dataStructure.matrix.DoubleMatrix;
40  import ubic.basecode.util.ConfigUtils;
41  
42  /**
43   * @author pavlidis
44   *
45   */
46  public class RServeClient extends AbstractRClient {
47  
48      /**
49       *
50       */
51      private static final int DEFAULT_PORT = 6311;
52  
53      private static final int MAX_CONNECT_TRIES = 10;
54  
55      private static final int MAX_EVAL_TRIES = 3;
56  
57      private final static String os = System.getProperty( "os.name" ).toLowerCase();
58  
59      /**
60       * @return
61       * @throws ConfigurationException
62       */
63      protected static String findRserveCommand() throws ConfigurationException {
64          URL userSpecificConfigFileLocation = ConfigUtils.locate( "local.properties" );
65  
66          PropertiesConfiguration userConfig = null;
67          if ( userSpecificConfigFileLocation != null ) {
68              userConfig = new PropertiesConfiguration();
69              FileHandler handler = new FileHandler( userConfig );
70              handler.setFileName( "local.properties" );
71              handler.load();
72          }
73          String rserveExecutable = null;
74          if ( userConfig != null ) {
75              rserveExecutable = userConfig.getString( "rserve.start.command" );
76          }
77          if ( StringUtils.isBlank( rserveExecutable ) ) {
78              log.info( "Rserve command not configured? Trying fallbacks" );
79              if ( os.startsWith( "windows" ) ) { // lower cased
80                  rserveExecutable = System.getenv( "R_HOME" ) + File.separator + "library" + File.separator + "Rserve"
81                          + File.separator + "Rserve.exe";
82              } else {
83                  rserveExecutable = "R CMD Rserve";
84              }
85          }
86          return rserveExecutable;
87      }
88  
89      private RConnection connection = null;
90  
91      /**
92       * Gets connection on default host (localhost) and port (6311)
93       *
94       * @throws IOException
95       */
96      protected RServeClient() throws IOException {
97          if ( !connect() ) {
98              throw new IOException( "Could not connect to Rserve" );
99          }
100     }
101 
102     /**
103      * @param host
104      * @throws IOException
105      */
106     protected RServeClient( String host ) throws IOException {
107         if ( !connect( host, DEFAULT_PORT ) ) {
108             throw new IOException( "Could not connect to Rserve" );
109         }
110     }
111 
112     /*
113      * (non-Javadoc)
114      *
115      * @see ubic.basecode.util.RClient#assign(java.lang.String, double[])
116      */
117     @Override
118     public void assign( String argName, double[] arg ) {
119         checkConnection();
120 
121         try {
122             connection.assign( argName, arg );
123         } catch ( REngineException e ) {
124             throw new RuntimeException( e );
125         }
126 
127     }
128 
129     /*
130      * (non-Javadoc)
131      *
132      * @see ubic.basecode.util.RClient#assign(java.lang.String, int[])
133      */
134     @Override
135     public void assign( String arg0, int[] arg1 ) {
136         if ( StringUtils.isBlank( arg0 ) ) {
137             throw new IllegalArgumentException( "Must supply valid variable name" );
138         }
139         checkConnection();
140         try {
141             connection.assign( arg0, arg1 );
142         } catch ( REngineException e ) {
143             throw new RuntimeException( e );
144         }
145     }
146 
147     /*
148      * (non-Javadoc)
149      *
150      * @see org.rosuda.JRclient.Rconnection#assign(java.lang.String, java.lang.String)
151      */
152     /*
153      * (non-Javadoc)
154      *
155      * @see ubic.basecode.util.RClient#assign(java.lang.String, java.lang.String)
156      */
157     @Override
158     public void assign( String sym, String ct ) {
159         if ( StringUtils.isBlank( sym ) ) {
160             throw new IllegalArgumentException( "Must supply valid variable name" );
161         }
162         try {
163             this.checkConnection();
164             connection.assign( sym, ct );
165         } catch ( RserveException e ) {
166             throw new RuntimeException( "Assignment failed: " + sym + " value " + ct, e );
167         }
168     }
169 
170     /*
171      * (non-Javadoc)
172      *
173      * @see ubic.basecode.util.RClient#assign(java.lang.String, java.lang.String[])
174      */
175     @Override
176     public void assign( String argName, String[] array ) {
177         if ( array == null || array.length == 0 ) {
178             throw new IllegalArgumentException( "Array must not be null or empty" );
179         }
180         if ( StringUtils.isBlank( argName ) ) {
181             throw new IllegalArgumentException( "Must supply valid variable name" );
182         }
183         try {
184             log.debug( "assign: " + argName + "<-" + array.length + " strings." );
185             this.checkConnection();
186             connection.assign( argName, array );
187         } catch ( REngineException e ) {
188             throw new RuntimeException( "Failure with assignment: " + argName + "<-" + array.length + " strings." + e );
189         }
190     }
191 
192     /**
193      *
194      *
195      */
196     public boolean connect() {
197         return connect( true );
198     }
199 
200     @Override
201     public void disconnect() {
202         if ( connection != null && connection.isConnected() ) connection.close();
203         connection = null;
204     }
205 
206     /*
207      * (non-Javadoc)
208      *
209      * @see ubic.basecode.util.r.RClient#eval(java.lang.String)
210      */
211     @Override
212     public REXP eval( String command ) {
213         log.debug( "eval: " + command );
214 
215         int lockValue = 0;
216         try {
217 
218             /*
219              * Failures due to communication? try repeatedly.
220              */
221             for ( int i = 0; i < MAX_EVAL_TRIES + 1; i++ ) {
222                 RuntimeException ex = null;
223                 try {
224                     checkConnection();
225                     lockValue = connection.lock();
226                     REXP r = connection.eval( "try(" + command + ", silent=T)" );
227                     if ( r == null ) return null;
228 
229                     if ( r.inherits( "try-error" ) ) {
230                         /*
231                          * This is not an eval error that would warrant a retry.
232                          */
233                         throw new RuntimeException( "Error from R: " + r.asString() );
234                     }
235                     return r;
236 
237                 } catch ( RserveException e ) {
238                     ex = new RuntimeException( "Error excecuting " + command + ": " + e.getMessage(), e );
239                 } catch ( REXPMismatchException e ) {
240                     throw new RuntimeException( "Error processing apparent error object returned by " + command + ": "
241                             + e.getMessage(), e );
242                 }
243 
244                 if ( i == MAX_EVAL_TRIES ) {
245                     throw ex;
246                 }
247 
248                 try {
249                     log.debug( "Eval failed, retrying" );
250                     Thread.sleep( 200 );
251                 } catch ( InterruptedException e ) {
252                     return null;
253                 }
254 
255             }
256 
257             throw new RuntimeException( "Evaluation failed! No details available" );
258         } finally {
259             if ( lockValue != 0 ) connection.unlock( lockValue );
260         }
261     }
262 
263     /*
264      * (non-Javadoc)
265      *
266      * @see java.lang.Object#finalize()
267      */
268     @Override
269     public void finalize() {
270         this.disconnect();
271     }
272 
273     /*
274      * (non-Javadoc)
275      *
276      * @see ubic.basecode.util.RClient#getLastError()
277      */
278     @Override
279     public String getLastError() {
280         return connection.getLastError();
281     }
282 
283     /**
284      *
285      */
286     @Override
287     public boolean isConnected() {
288         if ( connection != null && connection.isConnected() ) return true;
289         return false;
290     }
291 
292     /*
293      * (non-Javadoc)
294      *
295      * @see ubic.basecode.util.RClient#retrieveMatrix(java.lang.String)
296      */
297     @Override
298     public DoubleMatrix<String, String> retrieveMatrix( String variableName ) {
299         try {
300             log.debug( "Retrieving " + variableName );
301 
302             // REXP clr = this.eval( "class(" + variableName + ")" );
303             // log.info( clr.asString() );
304 
305             // note that for some reason, asDoubleMatrix is returning a 1-d array. So I do this.
306             REXP r = this.eval( "data.frame(t(" + variableName + "))" );
307             if ( r == null ) throw new IllegalArgumentException( variableName + " not found in R context" );
308 
309             RList dataframe = r.asList();
310             int numrows = dataframe.size();
311             double[][] results = new double[numrows][];
312             int i = 0;
313             for ( Iterator<?> it = dataframe.iterator(); it.hasNext(); ) {
314                 REXP next = ( REXP ) it.next();
315                 double[] row = next.asDoubles();
316                 results[i] = row;
317                 i++;
318             }
319 
320             DoubleMatrix<String, String> resultObject = new DenseDoubleMatrix<String, String>( results );
321 
322             retrieveRowAndColumnNames( variableName, resultObject );
323             return resultObject;
324         } catch ( REXPMismatchException e ) {
325             throw new RuntimeException( "Failed to get back matrix for variable " + variableName, e );
326         }
327 
328     }
329 
330     /*
331      * (non-Javadoc)
332      *
333      * @see ubic.basecode.util.RClient#voidEval(java.lang.String)
334      */
335     @Override
336     public void voidEval( String command ) {
337         if ( command == null ) throw new IllegalArgumentException( "Null command" );
338         this.checkConnection();
339 
340         log.debug( "voidEval: " + command );
341         eval( command );
342 
343     }
344 
345     /**
346      *
347      */
348     private void checkConnection() {
349         if ( !this.isConnected() ) {
350 
351             /*
352              * This often won't work. Even if we reconnect, state will have been lost. However, under normal
353              * circumstances (over a local network) connection loss should be very rare.
354              */
355             log.warn( "Not connected, trying to reconnect" );
356             boolean ok = false;
357             for ( int i = 0; i < MAX_CONNECT_TRIES; i++ ) {
358                 try {
359                     Thread.sleep( 200 );
360                 } catch ( InterruptedException e ) {
361                     return;
362                 }
363                 ok = this.connect();
364                 if ( ok ) break;
365             }
366             if ( !ok ) {
367                 throw new RuntimeException( "Not connected" );
368             }
369         }
370     }
371 
372     /**
373      * @param beQuiet
374      */
375     private boolean connect( boolean beQuiet ) {
376         if ( connection != null && connection.isConnected() ) {
377             return true;
378         }
379         int tries = 3;
380         Exception ex = null;
381         for ( int i = 0; i < tries; i++ ) {
382             try {
383                 connection = new RConnection();
384                 return true;
385             } catch ( RserveException e ) {
386                 ex = e;
387                 try {
388                     Thread.sleep( 100 );
389                 } catch ( InterruptedException e1 ) {
390                     return false;
391                 }
392             }
393         }
394         if ( !beQuiet ) {
395             log.error( "Could not connect to RServe: " + ( ex == null ? "" : ex.getMessage() ) );
396         }
397         return false;
398     }
399 
400     /**
401      * @param host
402      * @param port
403      * @return
404      */
405     private boolean connect( String host, int port ) {
406         if ( connection != null && connection.isConnected() ) {
407             return true;
408         }
409         try {
410             connection = new RConnection( host, port );
411         } catch ( RserveException e ) {
412             log.error( "Could not connect to RServe: " + e.getMessage() );
413             return false;
414         }
415         log.info( "Connected via RServe." );
416         return true;
417     }
418 
419     /**
420      * @param variableName
421      * @param resultObject
422      * @throws REXPMismatchException
423      */
424     private void retrieveRowAndColumnNames( String variableName, DoubleMatrix<String, String> resultObject ) {
425         List<String> rowNames = this.stringListEval( "dimnames(" + variableName + ")[1][[1]]" );
426 
427         if ( rowNames.size() == resultObject.rows() ) {
428             resultObject.setRowNames( rowNames );
429         }
430 
431         List<String> colNames = this.stringListEval( "dimnames(" + variableName + ")[2][[1]]" );
432 
433         if ( colNames.size() == resultObject.columns() ) {
434             resultObject.setColumnNames( colNames );
435         }
436     }
437 
438 }