/* * BioJava development code * * This code may be freely distributed and modified under the * terms of the GNU Lesser General Public Licence. This should * be distributed with the code. If you do not have a copy, * see: * * http://www.gnu.org/copyleft/lesser.html * * Copyright for this code is held jointly by the individual * authors. These should be listed in @author doc comments. * * For more information on the BioJava project and its aims, * or to join the biojava-l mailing list, visit the home page * at: * * http://www.biojava.org/ * */ /* * ExternalProcess.java */ package org.biojava.utils.process; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.io.StringWriter; import java.util.Enumeration; import java.util.Properties; import org.biojava.utils.SimpleThreadPool; import org.biojava.utils.ThreadPool; /** * Utility class to execute an external process and to handle * the STDOUT, STDERR and STDIN streams * in multiple threads managed by a thread pool. *

This class is intended for applications that call an external program many * times, e.g. in a loop, and that need high performance throughput, i.e. * the program's input and output should not be written to disk. The Java * {@link java.lang.Runtime#exec} methods requires the application to read/write * the external program's input and output streams in multiple threads. * Otherwise the calling application may block. However, instantiating multiple * threads for each call is extensive. On Linux systems there is also the * problem that each Java thread is represented by a single process and the * number of processes is limited on Linux. Because the Java garbage collector * does not free the {@link java.lang.Thread} objects properly, an application * might run out of threads (indicated by a {@link java.lang.OutOfMemoryError} * exception) after multiple iterations. Therefore, the * ExternalProcess class uses a * {@linkplain org.biojava.utils.ThreadPool thread pool}.

*

The simplest way to use this class is by calling the static methods * {@link #execute(String)} and * {@link #execute(String, String, StringWriter, StringWriter)}. However, these * methods are not thread safe and no configuration is possible. In the former * case the program's input, output and error output is redirected to STDIN, * STDOUT and STDERR of the calling program. In the * latter case input is provided as string and output and error output is * written to {@link java.io.StringWriter} objects. The environment, i.e. * the current working directory and the environment variables, are inherited * from the calling process. In both cases, a static thread pool of size * {@link #THREAD_POOL_SIZE} is used. The command that should be executed is * provided as a string argument.

*

In scenarios where the environment has to be changed, the program input * is generated just in time, or the program's output is parsed just in time, * the use of an explicit instance of the ExternalProcess class * is recommended. This instance could be initialized with a custom thread pool. * Otherwise a {@link org.biojava.utils.SimpleThreadPool} of size 3 is used. * The input and output is managed by multithreaded * {@linkplain org.biojava.utils.process.InputHandler input handler} and * {@linkplain org.biojava.utils.process.OutputHandler output handler} objects. * There are four predefined handlers that read the program's input from a * {@link java.io.Reader} object or a {@link java.io.InputStream} object and * write the program's output to a {@link java.io.Writer} object or a * {@link java.io.OutputStream} object. These classes are called: * {@link org.biojava.utils.process.ReaderInputHandler}, * {@link org.biojava.utils.process.SimpleInputHandler}, * {@link org.biojava.utils.process.WriterOutputHandler} and * {@link org.biojava.utils.process.SimpleOutputHandler}. If no handlers are * specified the input and output is redirected to the standards streams of * the calling process.

*

Before one of the methods {@link #execute()} or * {@link #execute(Properties)} is called, the {@linkplain #setCommands(String) * commands} property should be set. One may include placeholders of the form * %PARAM% within the commands. If a * {@link java.util.Properties} object is passed to the * {@link #execute(Properties)} method, the placeholders are replaced by the * particular property value. Therefore, the Properties object * must contain a key named PARAM (case doesn't matter). The * environment for calling the external program can be configured using the * properties {@linkplain #setWorkingDirectory(File) workingDirectory} and * {@linkplain #setEnvironmentProperties(String[]) environmentProperties}.

*

Finally, the {@linkplain #setSleepTime(int) sleepTime} property can be * increased, in case the output handlers are not able to catch the whole * program's output within the given time. The default value is * {@link #SLEEP_TIME} [in milliseconds].

* @author Martin Szugat * @version $Revision: 4023 $ * @see java.lang.Process */ public final class ExternalProcess { /* STATIC FIELDS */ /** * Size of the thread pool for the static execute methods. */ public static final int THREAD_POOL_SIZE = 9; /** * Number of milliseconds the execute method should pauses after the * external process has finished the execution. */ public static final int SLEEP_TIME = 10; /** * Static thread pool for static execute method. */ private static final ThreadPool THREAD_POOL = new SimpleThreadPool(THREAD_POOL_SIZE, true); /* MAIN METHOD */ /** * Runs an external program from the command line. The external process * inherits the environment variables and the current working directory from * the parent process. * @param args the path or the name of the external program and its command * line arguments */ public static void main(/*@non_null@*/ String[] args) { try { System.exit(execute(joinCommands(args))); } catch (Exception e) { e.printStackTrace(); } } /* STATIC METHODS */ /** * Resolves the given command line by replacing all placeholder of the * format %NAME% with the values from the given properties * for the corresponding keys of the format NAME. * @param commands the given command line * @param variables the placeholders or null if no resolvement * should be performed * @return the new command line * @throws NullPointerException if commands is * null. */ public static String resolveCommands(/*@non_null@*/ String commands, Properties variables) throws NullPointerException { if (commands == null) { throw new NullPointerException("commands is null."); } if (variables != null && !variables.isEmpty()) { Enumeration keys = variables.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); String value = variables.getProperty(key); // TODO quotting commands = commands.replaceAll("%" + key + "%", value); } } return commands; } /** * Executes an external program. The working directory and the environment * variables are inherited from the parent process. The program input is * read from STDIN, the program output is written to * STDOUT and the program error output is written to * STDERR. *

Note: This method is not thread-safe.

* @param commands the command line including the path * or the name of the external program and its command line arguments * @return the exit code from the external program * @throws SecurityException if a security manager exists and its * checkExec method doesn't allow creation of a subprocess. * @throws IOException if an I/O error occurs. * @throws NullPointerException if commands is * null. * @throws IllegalArgumentException if commandList is empty. * @throws InterruptedException if the current thread is * {@link Thread#interrupt() interrupted} by another thread * while it is waiting, then the wait is ended and an * InterruptedException is thrown. */ public static int execute(/*@non_null@*/ String commands) throws IOException, InterruptedException, NullPointerException, SecurityException, IllegalArgumentException { return execute(commands, null, null, null); } /** * Executes an external program. The working directory and the environment * variables are inherited from the parent process. *

Note: This method is not thread-safe.

* @param commands the command line including the path * or the name of the external program and its command line arguments * @param inputString the input for the external programm or * null if the input should be read from STDIN * @param outputString the output of the external programm or * null if the output should be written to STDOUT * @param errorString the error output of the external program or * null if the error output should be written to * STDERR * @return the exit code from the external program * @throws SecurityException if a security manager exists and its * checkExec method doesn't allow creation of a subprocess. * @throws IOException if an I/O error occurs. * @throws NullPointerException if commandList is * null. * @throws IllegalArgumentException if commandList is empty. * @throws InterruptedException if the current thread is * {@link Thread#interrupt() interrupted} by another thread * while it is waiting, then the wait is ended and an * InterruptedException is thrown. */ public static int execute(/*@non_null@*/ String commands, String inputString, StringWriter outputString, StringWriter errorString) throws IOException, InterruptedException, NullPointerException, SecurityException, IllegalArgumentException { ExternalProcess ep = new ExternalProcess(THREAD_POOL); ep.setCommands(commands); if (inputString != null) { ep.setInputHandler(new ReaderInputHandler( new StringReader(inputString), "STDIN")); } if (outputString != null) { ep.setOutputHandler(new WriterOutputHandler(outputString, "STDOUT")); } if (errorString != null) { ep.setErrorHandler(new WriterOutputHandler(errorString, "STDERR")); } return ep.execute(); } /** * Joins a command list to a single command string. * @param commandList the list of the command and its arguments * @return the joined command line * @throws NullPointerException if commandList is * null. */ public static String joinCommands(/*@non_null@*/ Object[] commandList) throws NullPointerException { if (commandList == null) { throw new NullPointerException("commandList is null."); } StringBuffer sb = new StringBuffer(); for (int i = 0; i < commandList.length; i++) { // TODO quoting if (i == commandList.length - 1) { sb.append(commandList[i].toString()); } else { sb.append(commandList[i].toString() + " "); } } return sb.toString(); } /* PRIVATE FIELDS */ /** * The command including the external program and its arguments. */ private String commands = null; /** * The working directory of the external program. */ private File workingDirectory = null; /** * The list of environment variables for the external program. */ private String[] environmentProperties = null; /** * The input handler for STDIN. */ private InputHandler inputHandler = null; /** * The output handler for STDOUT. */ private OutputHandler outputHandler = null; /** * The output handler for STDERR. */ private OutputHandler errorHandler = null; /** * The thread pool for the input and output handlers. */ private ThreadPool threadPool = null; /** * Should threads be stopped when finalized? */ private boolean stopThreads = true; /** * Number of seconds to wait for completion of stream handlers. */ private int sleepTime = SLEEP_TIME; /* PUBLIC CONSTRUCTORS */ /** * Initializes the external process. */ public ExternalProcess() { this(null); } /** * Initializes the external process. * @param threadPool a thread pool with at least three threads or * null if the default thread pool should be used */ public ExternalProcess(ThreadPool threadPool) { if (threadPool == null) { this.threadPool = new SimpleThreadPool(3, true); stopThreads = true; } else { this.threadPool = threadPool; stopThreads = false; } setEnvironmentProperties(null); setWorkingDirectory(null); setInputHandler(null); setOutputHandler(null); setErrorHandler(null); } /* PUBLIC METHODS */ /** * Executes the external process and waits for its termination. * @return the exit code from the external process * @throws IllegalArgumentException if the command is empty * @throws SecurityException if a security manager exists and its * checkExec method doesn't allow creation of a subprocess. * @throws IOException if an I/O error occurs. * @throws InterruptedException if the current thread is * {@link Thread#interrupt() interrupted} by another thread * while it is waiting, then the wait is ended and an * InterruptedException is thrown. */ public /*@pure@*/ int execute() throws IOException, InterruptedException, SecurityException, IllegalArgumentException { return execute((Properties) null); } /** * Executes the external process and waits for its termination. * @param variables a list of key-value-pairs that should be used to replace * placeholders in the command line. May be null. * @return the exit code from the external process * @throws SecurityException if a security manager exists and its * checkExec method doesn't allow creation of a subprocess. * @throws IllegalArgumentException if the command is empty * @throws IOException if an I/O error occurs. * @throws InterruptedException if the current thread is * {@link Thread#interrupt() interrupted} by another thread * while it is waiting, then the wait is ended and an * InterruptedException is thrown. */ public /*@pure@*/ int execute(Properties variables) throws IOException, InterruptedException, SecurityException, IllegalArgumentException { Runtime runtime = Runtime.getRuntime(); String commands = resolveCommands(this.commands, variables); Process process = runtime.exec(commands, environmentProperties, workingDirectory); OutputStream in = process.getOutputStream(); InputStream out = process.getInputStream(); InputStream err = process.getErrorStream(); inputHandler.setOutput(in); outputHandler.setInput(out); errorHandler.setInput(err); threadPool.addRequest(inputHandler); threadPool.addRequest(outputHandler); threadPool.addRequest(errorHandler); // start input and output handlers Thread.yield(); int exitCode = process.waitFor(); // give stream handlers time to complete Thread.sleep(sleepTime); in.close(); out.close(); err.close(); return exitCode; } /** * Gets the command line including the path or name of the external program * and its command line arguments. * @return the command line */ public /*@pure non_null@*/ String getCommands() { return commands; } /** * Sets the command line including the path or name of the external program * and its command line arguments. * @param commands the command line * @throws NullPointerException if commands is * null. */ public void setCommands(/*@non_null@*/ String commands) throws NullPointerException { if (commands == null) { throw new NullPointerException("commands is null."); } this.commands = commands; } /** * Gets environment variables for the external process. * @return a list of strings in the format * name=value or null if the environment variables * should be inherited from the parent process */ public /*@pure@*/ String[] getEnvironmentProperties() { String[] environmentProperties = null; if (this.environmentProperties != null) { environmentProperties = (String[]) this.environmentProperties.clone(); } return environmentProperties; } /** * Sets environment variables for the external process. * @param environmentProperties a list of strings in the format * name=value or null if the environment variables * should be inherited from the parent process */ public void setEnvironmentProperties(String[] environmentProperties) { if (environmentProperties == null) { this.environmentProperties = null; } else { this.environmentProperties = (String[]) environmentProperties.clone(); } } /** * Gets the output error handler which is responsible for the standard error * output of the external process. * @return the error output handler */ public /*@pure non_null@*/ OutputHandler getErrorHandler() { return errorHandler; } /** * Sets the output error handler which is responsible for the standard error * output of the external process. * @param errorHandler the error output handler or null if the * error output should be redirected to STDERR */ public void setErrorHandler(OutputHandler errorHandler) { if (errorHandler == null) { this.errorHandler = new SimpleOutputHandler(System.err, "STDERR"); } else { this.errorHandler = errorHandler; } } /** * Gets the input handler which is responsible for the standard input * of the external process. * @return the input handler */ public /*@pure non_null@*/ InputHandler getInputHandler() { return inputHandler; } /** * Sets the input handler which is responsible for the standard input * of the external process. * @param inputHandler the input handler or null if the * input should be read from STDIN */ public void setInputHandler(InputHandler inputHandler) { if (inputHandler == null) { this.inputHandler = new SimpleInputHandler(System.in, "STDINS"); } else { this.inputHandler = inputHandler; } } /** * Gets the output handler which is responsible for the standard output * of the external process. * @return the output handler */ public /*@pure non_null@*/ OutputHandler getOutputHandler() { return outputHandler; } /** * Sets the output handler which is responsible for the standard output * of the external process. * @param outputHandler the output handler or null if the * output should be redirected to STDOUT */ public void setOutputHandler(OutputHandler outputHandler) { if (outputHandler == null) { this.outputHandler = new SimpleOutputHandler(System.out, "STDOUT"); } else { this.outputHandler = outputHandler; } } /** * Gets the thread pool which is used for the input and output handlers. * @return a thread pool with at least three threads */ public /*@pure non_null@*/ ThreadPool threadPool() { return threadPool; } /** * Gets the working directory for the external process. * @return the working directory or null if it should be * inherited from the parent process */ public /*@pure@*/ File getWorkingDirectory() { return workingDirectory; } /** * Sets the working directory for the external process. * @param workingDirectory the working directory or null if it * should be inherited from the parent process */ public void setWorkingDirectory(File workingDirectory) { this.workingDirectory = workingDirectory; } /*@ ensures \result >= 0; @*/ /** * Gets the number of milliseconds the {@linkplain #execute(Properties)} * method should pauses after the external process is terminated. This gives * the stream handlers the time to complete their work. * @return time in milliseconds */ public /*@pure@*/ int getSleepTime() { return sleepTime; } /*@ requires sleepTime >= 0; @*/ /** * Sets the number of milliseconds the {@linkplain #execute(Properties)} * method should pauses after the external process is terminated. Increase * this value if the output handlers didn't catch the whole program output. * @param sleepTime time in milliseconds * @throws IllegalArgumentException if sleepTime is negative. */ public void setSleepTime(int sleepTime) throws IllegalArgumentException { if (sleepTime < 0.0) { throw new IllegalArgumentException( "sleepTime must be zero or positive."); } this.sleepTime = sleepTime; } /* CLASS Object */ /** * {@inheritDoc} */ protected void finalize() { if (stopThreads) { threadPool.stopThreads(); } } }