/* * Copyright (c) 2009 The Jackson Laboratory * * This software was developed by Gary Churchill's Lab at The Jackson * Laboratory (see http://research.jax.org/faculty/churchill). * * This is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software. If not, see <http://www.gnu.org/licenses/>. */ package org.jax.r.jriutilities; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import org.jax.r.RCommand; import org.jax.r.RException; import org.jax.r.SimpleRCommand; import org.jax.util.concurrent.AbstractLongRunningTask; import org.jax.util.concurrent.SettableFuture; import org.rosuda.JRI.REXP; import org.rosuda.JRI.RMainLoopCallbacks; import org.rosuda.JRI.Rengine; /** * A basic implementation of the R interface functionality that * we want. See {@link RInterface} for a more detailed description * of the requirements that this class fulfills. * @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A> */ public class BasicRInterface extends AbstractLongRunningTask implements RInterface { /** * our logger */ private static final Logger LOG = Logger.getLogger(BasicRInterface.class.getName()); /** * a dummy command to flush the queue */ private static final RCommand FLUSH_COMMAND = new SilentRCommand( "\"finished flushing R commands\""); /** * arguments that we should pass to the R enging */ // TODO add to properties/config file when we have one private static final String[] R_ENGINE_ARGS = new String[] {"--save"}; /** * the prefix that we append to comment strings */ private static final String R_COMMENT_PREFIX = "# "; /** * the list of listeners */ private final ConcurrentLinkedQueue<RInterfaceListener> listenerList; /** * Our R loop. */ private final RMainLoopCallbacks rCallBacks; /** * the thread safe R command queue */ private final BlockingQueue<AnyRInput> commandQueue; /** * holds the pending command count */ private final AtomicInteger pendingCommandCounter; /** * Our interface to R. */ @SuppressWarnings("unused") private Rengine rEngine; /** * flag that determines whether or not the R engine has been started yet */ private volatile boolean rHasBeenStarted; /** * Constructor. I made this package protected because we need to get to this * from the factory method. We can't allow more than one instance to be * created... see {@link org.rosuda.JRI.Rengine} for explanation of why * we can't have 2 R engines. */ /*package-protected*/ BasicRInterface() { this.rHasBeenStarted = false; this.commandQueue = new LinkedBlockingQueue<AnyRInput>(); this.rCallBacks = new RMainLoopCallBacksImpl(); this.listenerList = new ConcurrentLinkedQueue<RInterfaceListener>(); this.pendingCommandCounter = new AtomicInteger(0); } /** * just delegate to the R engine. the whole reason * for this method is that it's not good practice to register * for callbacks in the constructor if there is a chance * they will get called before initialization completes. */ private void startR() { this.rEngine = new Rengine( R_ENGINE_ARGS, true, this.rCallBacks); } /** * {@inheritDoc} */ public void flushCommands() { this.evaluateCommand(FLUSH_COMMAND); } /** * {@inheritDoc} */ public REXP evaluateCommand(String command) throws RException { return this.evaluateCommand(new SimpleRCommand(command)); } /** * {@inheritDoc} */ public Future<REXP> evaluateCommandAsynchronous(String command) throws RException { return this.evaluateCommandAsynchronous(new SimpleRCommand(command)); } /** * {@inheritDoc} */ public void evaluateCommandNoReturn(String command) throws RException { this.evaluateCommandNoReturn(new SimpleRCommand(command)); } /** * {@inheritDoc} */ public REXP evaluateCommand(RCommand command) throws RException { try { // do the lazy thing and delegate to the asynchronous method return this.evaluateCommandAsynchronous(command).get(); } catch(Exception ex) { // pass the buck on this exception throw new RException(ex); } } /** * {@inheritDoc} */ public synchronized Future<REXP> evaluateCommandAsynchronous(RCommand command) throws RException { if(!this.rHasBeenStarted) { this.rHasBeenStarted = true; this.startR(); } // increment the pending command count this.pendingCommandCounter.incrementAndGet(); this.fireChangeEvent(); AnyRInput input = new AnyRInput(command, AnyRInput.InputType.COMMAND_NEEDS_RETURN); this.commandQueue.add(input); return input.getAssociatedResult(); } /** * {@inheritDoc} */ public synchronized void evaluateCommandNoReturn(RCommand command) { if(!this.rHasBeenStarted) { this.rHasBeenStarted = true; this.startR(); } // increment the pending command count this.pendingCommandCounter.incrementAndGet(); this.fireChangeEvent(); AnyRInput input = new AnyRInput(command, AnyRInput.InputType.COMMAND_NO_RETURN); this.commandQueue.add(input); } /** * {@inheritDoc} */ public void insertComment(String comment) { this.insertCommentVerbatim(R_COMMENT_PREFIX + comment + '\n'); } /** * {@inheritDoc} */ public synchronized void insertCommentVerbatim(String verbatimComment) { if(!this.rHasBeenStarted) { this.rHasBeenStarted = true; this.startR(); } AnyRInput input = new AnyRInput(verbatimComment, AnyRInput.InputType.COMMENT); this.commandQueue.add(input); } /** * {@inheritDoc} */ public boolean isAnyCommandPending() { // check the counter for any pending commands return this.pendingCommandCounter.get() > 0; } /** * {@inheritDoc} */ public void addRInterfaceListener(RInterfaceListener listenerToAdd) { if(listenerToAdd == null) { throw new NullPointerException( "can't have a null listener"); } this.listenerList.add(listenerToAdd); } /** * {@inheritDoc} */ public void removeRInterfaceListener(RInterfaceListener listenerToRemove) { this.listenerList.remove(listenerToRemove); } /** * Notify listeners that the pending command count has changed * @param updatedCommandCount * the updated count */ private void firePendingCommandCountChanged(int updatedCommandCount) { for(RInterfaceListener currListener: this.listenerList) { currListener.pendingCommandCountChanged(updatedCommandCount); } } /** * tell all the listeners that command processing completed * @see RInterfaceListener#completedCommandProcessing(RInterface, RCommand, REXP) */ private void fireCompletedCommandProcessing(RCommand command, REXP result) { for(RInterfaceListener currListener: this.listenerList) { currListener.completedCommandProcessing(this, command, result); } } /** * tell all listeners that we've received a comment * @param comment * the comment that we've received */ private void fireReceivedComment(String comment) { for(RInterfaceListener currListener: this.listenerList) { currListener.receivedComment(comment); } } /** * tell all the listeners that command processing initiated * @see RInterfaceListener#initiatedCommandProcessing(RInterface, RCommand) */ private void fireInitiatedCommandProcessing(RCommand command) { for(RInterfaceListener currListener: this.listenerList) { currListener.initiatedCommandProcessing(this, command); } } /** * tell all the listeners that we received a message from R * @see RInterfaceListener#receivedMessageFromR(RInterface, String, RCommand) */ private void fireReceivedMessageFromR(String message, RCommand activeCommand) { for(RInterfaceListener currListener: this.listenerList) { currListener.receivedMessageFromR(this, message, activeCommand); } } /** * tell all the listeners that we received output from R * @see RInterfaceListener#receivedOutputFromR(RInterface, String, RCommand) */ private void fireReceivedOutputFromR(String output, RCommand activeCommand) { for(RInterfaceListener currListener: this.listenerList) { currListener.receivedOutputFromR(this, output, activeCommand); } } /** * Holds all of our input types so that we can send them down the * same Queue. */ private static class AnyRInput { /** * for differentiating between different input types */ public static enum InputType { /** * a command that needs a return value */ COMMAND_NEEDS_RETURN, /** * a command that doesn't need a return value */ COMMAND_NO_RETURN, /** * just a comment */ COMMENT } /** * @see #getInput() */ private final Object input; /** * @see #getInputType() */ private final InputType inputType; /** * @see #getAssociatedResult() */ private final SettableFuture<REXP> associatedResult; /** * Constructor. you give us the R command, we make the future * result * @param input * the input * @param inputType * the input type */ public AnyRInput(Object input, InputType inputType) { this.input = input; this.inputType = inputType; if(inputType == InputType.COMMAND_NEEDS_RETURN) { this.associatedResult = new SettableFuture<REXP>(); } else { this.associatedResult = null; } } /** * Getter for the result... only valid if we * have a command type that's looking for a * result * @return the associatedResult */ public SettableFuture<REXP> getAssociatedResult() { return this.associatedResult; } /** * @return the input */ public Object getInput() { return this.input; } /** * @return the inputType */ public InputType getInputType() { return this.inputType; } } /** * Our private R callback class... */ private class RMainLoopCallBacksImpl implements RMainLoopCallbacks { /** * Holds our pending command */ private RCommand pendingCommand = null; /** * Called when R transitions to and from "working" mode * @param rEngine * the R engine that called us * @param which * whether we've entered or exited "busy" state */ public void rBusy(Rengine rEngine, int which) { if(LOG.isLoggable(Level.FINE)) { LOG.fine("R busy status changed to " + which); } } /** * Called when R wants us to choose a file (not implemented). * @param rEngine * the R engine that called us * @param newFile * indicates if we should select a new or existing file * @return * the file that we chose */ public String rChooseFile(Rengine rEngine, int newFile) { LOG.warning("R is asking us to choose a file, but we don't do that"); return null; } /** * R wants us to flush any buffered output (not implemented). * @param rEngine * the R engine calling us */ public void rFlushConsole(Rengine rEngine) { if(LOG.isLoggable(Level.FINE)) { LOG.fine("R is asking us to flush console output"); } } /** * R wants us to load history from the given file * @param rEngine * the R engine calling us * @param filename * the filename that we're supposed to load from */ public void rLoadHistory(Rengine rEngine, String filename) { LOG.warning( "R is asking us to load history from \"" + filename + "\", but we don't do that"); } /** * This callback allows us to send input to R. * @param rEngine * the R engine * @param prompt * the prompt string that we should use * @param addToHistory * tells us whether we should be keeping history or not * @return * the command string for R */ public String rReadConsole(Rengine rEngine, String prompt, int addToHistory) { // see if we just finished a pending command if(this.pendingCommand != null) { // notify listeners that we're done BasicRInterface.this.pendingCommandCounter.decrementAndGet(); BasicRInterface.this.fireChangeEvent(); BasicRInterface.this.fireCompletedCommandProcessing( this.pendingCommand, null); // set the pending command to null this.pendingCommand = null; } try { while(this.pendingCommand == null) { BasicRInterface.this.firePendingCommandCountChanged( BasicRInterface.this.commandQueue.size()); // read the next input item off the queue AnyRInput input = BasicRInterface.this.commandQueue.take(); BasicRInterface.this.firePendingCommandCountChanged( BasicRInterface.this.commandQueue.size() + 1); if(input.inputType == AnyRInput.InputType.COMMENT) { // notify listeners BasicRInterface.this.fireReceivedComment( (String)input.getInput()); if(LOG.isLoggable(Level.FINE)) { LOG.fine("R Comment: " + input.getInput()); } } else if(input.inputType == AnyRInput.InputType.COMMAND_NEEDS_RETURN) { RCommand rCommand = (RCommand)input.getInput(); String command = rCommand.getCommandText() + "\n"; // notify listeners that we've started BasicRInterface.this.fireInitiatedCommandProcessing( rCommand); if(LOG.isLoggable(Level.FINE)) { LOG.fine("R Command (requires return): " + command); } // have R do its thing SettableFuture<REXP> associatedResult = input.getAssociatedResult(); REXP result = rEngine.eval(command, true); associatedResult.set(result); // notify listeners that we're done BasicRInterface.this.pendingCommandCounter.decrementAndGet(); BasicRInterface.this.fireChangeEvent(); BasicRInterface.this.fireCompletedCommandProcessing( rCommand, result); } else if(input.inputType == AnyRInput.InputType.COMMAND_NO_RETURN) { RCommand rCommand = (RCommand)input.getInput(); // notify listeners that we've started BasicRInterface.this.fireInitiatedCommandProcessing(rCommand); if(LOG.isLoggable(Level.FINE)) { LOG.fine("R Command (no return): " + rCommand); } // use the R loop to process this command since we don't need a // return value. the advantage of doing it this way is that we // get R output text if there is any (rEngine.eval won't do // that for us this.pendingCommand = rCommand; } } } catch(Exception ex) { LOG.log(Level.SEVERE, "received an exception while trying to evaluate R input", ex); } return this.pendingCommand.getCommandText() + "\n"; } /** * R wants us to "save history" to the given file. We ignore * this one. * @param rEngine * the R engine * @param filename * the file that we're supposed to save history to */ public void rSaveHistory(Rengine rEngine, String filename) { LOG.warning( "we're being asked by R to save our history to \"" + filename + "\", but we don't do that"); } /** * Show the given warning message from R * @param rEngine * the R engine that generated the warning message * @param message * the message */ public void rShowMessage(Rengine rEngine, String message) { LOG.warning( "received the following R message \"" + message + "\""); BasicRInterface.this.fireReceivedMessageFromR( message, this.pendingCommand); } /** * For receiving R's output text. * @param rEngine * the R engine calling us * @param text * the text output to write * @param type * the type */ public void rWriteConsole(Rengine rEngine, String text, int type) { if(LOG.isLoggable(Level.FINE)) { LOG.fine("R output: \"" + text + "\""); } BasicRInterface.this.fireReceivedOutputFromR( text, this.pendingCommand); } } /* * long-running task functions */ /** * {@inheritDoc} */ public String getTaskName() { return "Evaluating R Command"; } /** * {@inheritDoc} */ public int getTotalWorkUnits() { return this.pendingCommandCounter.get(); } /** * {@inheritDoc} */ public int getWorkUnitsCompleted() { return 0; } }