Command.java

package gov.usgs.earthquake.distribution;

import gov.usgs.util.StreamUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;

public class Command {

	private String[] commandArray = null;
	private String[] envp = null;
	private File dir = null;

	private InputStream stdin = null;
	private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
	private ByteArrayOutputStream stderr = new ByteArrayOutputStream();

	private long timeout = 0;
	private int exitCode = -1;

	/** Empty command constructor */
	public Command() {
	}

	/**
	 * @throws CommandTimeout CommandTimeout
	 * @throws IOException IOException
	 * @throws InterruptedException InterruptedException
	 */
	public void execute() throws CommandTimeout, IOException,
			InterruptedException {
		StreamTransferThread outputTransfer = null;
		StreamTransferThread errorTransfer = null;

		try {
			final Process process = Runtime.getRuntime().exec(commandArray,
					envp, dir);

			final Timer timer;
			if (timeout > 0) {
				timer = new Timer();
				timer.schedule(new TimerTask() {
					public void run() {
						process.destroy();
					}
				}, timeout);
			} else {
				timer = null;
			}

			try {
				OutputStream processStdin = process.getOutputStream();
				if (stdin != null) {
					StreamUtils.transferStream(stdin, processStdin);
				}
				StreamUtils.closeStream(processStdin);

				outputTransfer = new StreamTransferThread(process.getInputStream(),
						stdout);
				outputTransfer.start();
				errorTransfer = new StreamTransferThread(process.getErrorStream(),
						stderr);
				errorTransfer.start();

				// now wait for process to complete
				exitCode = process.waitFor();
				if (exitCode == 143) {
					throw new CommandTimeout();
				}
			} finally {
				// cancel destruction of process, if it hasn't already run
				if (timer != null) {
					timer.cancel();
				}
			}
		} finally {
			try {
				outputTransfer.interrupt();
				outputTransfer.join();
			} catch (Exception e) {
			}
			try {
				errorTransfer.interrupt();
				errorTransfer.join();
			} catch (Exception e) {
			}
		}
	}

	/** @return string[] */
	public String[] getCommand() {
		return commandArray;
	}

	/**	@param command single command */
	public void setCommand(final String command) {
		setCommand(splitCommand(command));
	}

	/** @param commandArray string[] */
	public void setCommand(final String[] commandArray) {
		this.commandArray = commandArray;
	}

	/** @return envp */
	public String[] getEnvp() {
		return envp;
	}

	/** @param envp String[] */
	public void setEnvp(final String[] envp) {
		this.envp = envp;
	}

	/** @return dir */
	public File getDir() {
		return dir;
	}

	/** @param dir File */
	public void setDir(final File dir) {
		this.dir = dir;
	}

	/** @return timeout */
	public long getTimeout() {
		return timeout;
	}

	/** @param timeout long */
	public void setTimeout(final long timeout) {
		this.timeout = timeout;
	}

	/** @return exitCode */
	public int getExitCode() {
		return exitCode;
	}

	/** @param stdin InputStream */
	public void setStdin(final InputStream stdin) {
		this.stdin = stdin;
	}

	/** @return stdout byte[] */
	public byte[] getStdout() {
		return stdout.toByteArray();
	}

	/** @return stderr byte[] */
	public byte[] getStderr() {
		return stderr.toByteArray();
	}

	/**
	 * Split a command string into a command array.
	 *
	 * This version uses a StringTokenizer to split arguments. Quoted arguments
	 * are supported (single or double), with quotes removed before passing to
	 * runtime. Double quoting arguments will preserve quotes when passing to
	 * runtime.
	 *
	 * @param command
	 *            command to run.
	 * @return Array of arguments suitable for passing to
	 *         Runtime.exec(String[]).
	 */
	protected static String[] splitCommand(final String command) {
		List<String> arguments = new LinkedList<String>();
		String currentArgument = null;

		// use a tokenizer because that's how Runtime.exec does it currently...
		StringTokenizer tokens = new StringTokenizer(command);
		while (tokens.hasMoreTokens()) {
			String token = tokens.nextToken();

			if (currentArgument == null) {
				currentArgument = token;
			} else {
				// continuing previous argument, that was split on whitespace
				currentArgument = currentArgument + " " + token;
			}

			if (currentArgument.startsWith("\"")) {
				// double quoted argument
				if (currentArgument.endsWith("\"")) {
					// that has balanced quotes
					// remove quotes and add argument
					currentArgument = currentArgument.substring(1,
							currentArgument.length() - 1);
				} else {
					// unbalanced quotes, keep going
					continue;
				}
			} else if (currentArgument.startsWith("'")) {
				// single quoted argument
				if (currentArgument.endsWith("'")) {
					// that has balanced quotes
					// remove quotes and add argument
					currentArgument = currentArgument.substring(1,
							currentArgument.length() - 1);
				} else {
					// unbalanced quotes, keep going
					continue;
				}
			}

			arguments.add(currentArgument);
			currentArgument = null;
		}

		if (currentArgument != null) {
			// weird, but add argument anyways
			arguments.add(currentArgument);
		}

		return arguments.toArray(new String[] {});
	}

	public static class CommandTimeout extends Exception {

		private static final long serialVersionUID = 1L;

	}

	private static class StreamTransferThread extends Thread {
		private InputStream in;
		private OutputStream out;

		public StreamTransferThread(final InputStream in, final OutputStream out) {
			this.in = in;
			this.out = out;
		}

		@Override
		public void run() {
			try {
				StreamUtils.transferStream(in, out);
			} catch (Exception e) {
			}
		}
	}

}